Jstack的基本使用与线程介绍

介绍

jstack简直给我找到了一扇打开jvm的门。
jstack用于生成java虚拟机当前时刻的线程快照。
线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合。
可以帮助自己分析线程与线程的组织。定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
通过查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
可以直观的看到程序在电脑中的情况,在之前的学习时,都是抽象的,现在是具体的数据。

基本使用

1
jstack <进程id>

示例

如下面的java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {

public static void doAccept() throws IOException {
ServerSocket ss = new ServerSocket(1234);
Socket clientsocket = ss.accept();
clientsocket.close();
ss.close();
}

public static void main(String[] args) throws IOException {
doAccept();
}
}

代码简单的新建服务器socket,等待连接

运行

这个工程目录的结构是maven的普通java工程目录。

1
2
3
mvn compile
cd target/classes/
java jstack.Demo

查看进程

使用jps可以查看到当前的java进程
可以会如下显示

1
2
3
4
5
6
C:\>jps
328 Jps
5032 Demo
6364

C:\>

  • 328 为jps 自己的进程
  • Demo的进程id为5032
  • 6364 为自己电脑eclipse的进程

使用jstack查调用栈信息

1
jstack 5032

当然结果很长,重定向到文件中,便于查看jstack 5032 >stackinfo.txt

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):

"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000000001d92d000 nid=0x134 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #9 daemon prio=9 os_prio=2 tid=0x000000001d8ac000 nid=0x171c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x000000001d89d800 nid=0x140 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x000000001d896800 nid=0x1894 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x000000001d889800 nid=0x1444 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001d886800 nid=0x1b04 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001d886000 nid=0x1944 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001c7be000 nid=0x2d4 in Object.wait() [0x000000001e8cf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b408e98> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(Unknown Source)
- locked <0x000000076b408e98> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(Unknown Source)
at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001c7b7800 nid=0xfd8 in Object.wait() [0x000000001ec3f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b406b40> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Unknown Source)
at java.lang.ref.Reference.tryHandlePending(Unknown Source)
- locked <0x000000076b406b40> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)

"main" #1 prio=5 os_prio=0 tid=0x00000000026ff000 nid=0x1a04 runnable [0x00000000029df000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(Unknown Source)
at java.net.AbstractPlainSocketImpl.accept(Unknown Source)
at java.net.PlainSocketImpl.accept(Unknown Source)
- locked <0x000000076b4e0430> (a java.net.SocksSocketImpl)
at java.net.ServerSocket.implAccept(Unknown Source)
at java.net.ServerSocket.accept(Unknown Source)
at jstack.Demo.doAccept(Demo.java:11)
at jstack.Demo.main(Demo.java:17)

"VM Thread" os_prio=2 tid=0x000000001d822000 nid=0x1908 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000000000265c800 nid=0x790 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000265e000 nid=0x3d0 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000265f800 nid=0x1630 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002663000 nid=0x1af8 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000002664000 nid=0xa70 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000002665800 nid=0x79c runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000002668800 nid=0x500 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000002669800 nid=0x1b2c runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001d94b800 nid=0x340 waiting on condition

JNI global references: 15

内容分析

其中每一块都是一个线程的调用状态的快照。
比如第42行。主线程。
"main" #1 prio=5 os_prio=0 tid=0x00000000026ff000 nid=0x1a04 runnable [0x00000000029df000]

  • main 为线程名
  • prio 线程优先级
  • os_prio 操作系统层次的优先级
  • tid 线程标识
  • nid 线程id

42行下面的就是调用栈

这个进程在windows上显示有21个线程。

各线程功能与作用

GC

类似GC task thread#X (ParallelGC)这样的,是jvm 用来回收内存的。
它的线程数量与计算的cpu数量一样。

CompilerThread

类似"C2 CompilerThread1" #7 daemon这样的线程
它用来调用JITing,实时编译装卸class。通常,jvm会启动多个线程来处理这部分工作,线程名称后面的数字也会累加。

C2 Compiler 是JVM在server模式下字节码编译器,JVM启动的时候所有代码都处于解释执行模式,当某些代码被执行到一定阈值次数,这些代码(称为热点代码)就会被 C2 Compiler编译成机器码,编译成机器码后执行效率会得到大幅提升。

数量也是与可用CPU数量相关

Attach Listener

Attach Listener线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。比如jmap、jstack,都是通过这个线程进行交互的。

Signal Dispatcher

这个线程是用来处理外部命令的,比如你按Ctrl+C程序会退出。
前面的Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果

Finalizer

这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:1)只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;2)该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;3) JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;4) JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难;

Reference Handler

JVM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。

VM Thread

看名字就知道它很特殊,它是jvm里面的线程母体,这个单个的VM线程是会被其他线程所使用来做一些VM操作(如,清扫垃圾等)。在 VM Thread的结构体里有一个VMOperationQueue列队,所有的VM线程操作(vm_operation)都会被保存到这个列队当中,VMThread本身就是一个线程,它的线程负责执行一个自轮询的loop函数(具体可以参考:VMThread.cpp里面的void VMThread::loop()),该loop函数从VMOperationQueue列队中按照优先级取出当前需要执行的操作对象(VM_Operation),并且调用VM_Operation->evaluate函数去执行该操作类型本身的业务逻辑。

VM Periodic Task Thread

线程是JVM周期性任务调度的线程,它由WatcherThread创建,是一个单例对象。该线程在JVM内使用得比较频繁,比如:定期的内存监控、JVM运行状况监控,还有我们经常需要去执行一些jstat这类命令查看gc的情况

关于daemon 线程

在Java中有两类线程:

  • User Thread(用户线程)
  • Daemon Thread(守护线程)

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;
只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

参考

jstack oracle 帮助中心
Java占用高CPU分析之- C2 CompilerThread
C2 CompilerThread in java
How to reduce the number of threads used by the jvm
Understanding the Reference Handler thread
JVM 内部运行线程介绍
Java中守护线程的总结
Java命令学习系列——Jstack