探秘 JVM:监控与诊断工具

JDK 在给提供基础的 java 依赖库的同时,也在 bin 目录下提供了一系列的小工具,除了我们常用的 java 和 javac 以外,还包含许多对 JVM 进行性能监控和故障诊断的工具。这些工具能够为我们日常程序开发和问题排查提供极大的便利,主要包含以下几种:

工具 描述
jps 显示系统运行中的 JVM 进程列表
jstat 用于收集 JVM 各方面的运行数据(类加载、GC、JIT 编译等)
jinfo 查看和编辑 JVM 配置信息(JVM 启动参数、系统环境变量等)
jmap 生成 JVM 的堆转储快照(heapdump 文件)
jhat 用于分析 jmp 命令生成的 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果
jstack 生成 JVM 的线程转储快照(通常所说的 threaddump 文件或 javacore 文件),用于判断是否存在死锁、死循环,以及阻塞等情况
jconsole 可视化 java 监视与管理控制台,基于 JMX
jvisualvm 多合一故障处理工具,jconsole 的增强版,自 JDK 9 开始不再随 JDK 默认提供,需要独立下载,地址:http://visualvm.github.io/

补充:

  • jcmd :Java Command,JDK 7 引入,可以用来替代除 jstat 之外的所有命令。
  • jmc :Java Mission Control,JDK 7 引入,用于监控和管理 java 应用程序,且不会引入额外的性能开销。
  • jhsdb :Java HotSpot Debugger,JDK 9 引入,基于 Serviceability Agent 实现的 HotSpot 进程调试器。

上面这些工具都是对 jdk/lib/tools.jar 类库的上层封装,我们也可以在应用程序中利用该工具进行 JVM 监控分析。需要注意的是,tools 中的类库不属于 java 标准 API,也就是说不是所有的虚拟机都默认支持,如果不支持则需要随项目一起部署。此外,部分工具基于 JMX 实现,如果是在 JDK 5 上运行,在程序启动时需要添加 -Dcom.sun.management.jmxremote 参数以开启 JMX 管理功能。

一些 注意事项

  • 命令 jinfo、jstack,以及 jcmd 依赖于 JVM 的 Attach API,所以只能监控本地 JVM 进程。一旦设置了 JVM 的 DisableAttachMechanism 特性(可以通过 -XX:+DisableAttachMechanism 参数配置),基于 Attach API 的命令将无法执行。
  • 如果 JVM 进程关闭了 UsePerfData 参数(默认开启,可以通过 -XX:-UsePerfData 参数配置),则 jps 命令和 jstat 命令将无法探知该 JVM 进程。

命令行工具

jps

命令 jps 用于显示正在运行的 JVM 进程信息,以及这些进程对应的本地虚拟机唯一 ID(LVMID: Local Virtual Machine Identifier),可以类比 linux 的 ps 命令,不过相对来说功能要简单一些。对于本地虚拟机进程来说,LVMID 与操作系统的进程 ID 一致。默认情况下,jps 将显示当前正在运行的 JVM 进程 ID 和主类名信息。

1
2
3
4
5
6
7
jps [参数] [hostid]

参数说明:
-q: 只显示 LVMID,省略主类名
-m: 输出启动时传递给 main 函数的参数
-l: 输出主类的全名,如果执行的是 jar 包,则输出 jar 路径
-v: 输出启动时的 JVM 参数

示例:

1
2
3
$ jps -lm
22513 org.zhenchao.jvm.archangel.inspect.ArchangelDriver -r
13555 org.zhenchao.jvm.archangel.data.ArchangelDataDriver -r
1
2
$ jps -v
22513 ArchangelDriver -Xms2048m -Xmx4096m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -Dapp.name=archangel_inspector.sh -Dapp.pid=22513 -Dapp.repo=/home/work/bin/archangel-inspector/lib -Dapp.home=/home/work/bin/archangel-inspector -Dbasedir=/home/work/bin/archangel-inspector

如果希望通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程列表,可以通过设置 hostid 参数为 RMI 注册表中注册的主机名执行 jps 查询。

jstat

命令 jstat 用于监视 JVM 各种运行状态数据,包括本地或远程虚拟机进程中的类装载、内存、垃圾收集,以及 JIT 编译等运行数据,是在没有图形界面环境中执行性能监控和分析的首选工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jstat [参数] [vmid] [时间间隔(单位:s 或 ms)] [执行次数]

参数说明:
-class: 监视已加载类数目和内存占用、已卸载类数目和内存占用,以及类装载所耗费的时间
-compiler: 输出 JIT 编译过的方法和耗时等信息
-printcompilation: 输出已被 JIT 编译的方法
-gc: 监视 java 堆的使用情况,包括 Eden 区、两个 survivor 区、老年代、永久代等的容量,已用空间,GC 时间等信息
-gccapacity: 监视内容同 -gc,但输出主要关注 java 堆各个区域使用到的最大和最小空间
-gcutil: 监视内容同 -gc,但输出主要关注已使用空间占总空间的百分比
-gaccause: 相对于 -gcutil,会额外输出导致上一次 GC 产生的原因
-gcnew: 监视新生代的 GC 状况
-gcnewcapacity: 监视内容同 -gcnew,但是主要关注使用到的最大和最小空间
-gcold: 监视老年代的 GC 情况
-gcoldcapacity: 监视内容同 -gcold,但是主要关注使用到的最大和最小空间
-gcpermcapacity: 监视永久代使用到的最大和最小空间

说明 :如果是本地虚拟机进程,则 VMID 与 LVMID 是一致的,如果是远程虚拟机进程,则 VMID 的格式为 [protocol:][//]lvmid[@hostname[:port]/servername]

默认情况下,jstat 命令只会打印一次监控数据,可以设置时间间隔和执行次数以实现每隔一段时间打印一次,直至目标 JVM 进程终止为止,或者达到最大打印次数。

  • 打印进程的类加载情况
1
2
3
4
5
6
7
$ jstat -class 22513 1s 5
Loaded Bytes Unloaded Bytes Time
9778 19180.7 55 79.3 5.29
9778 19180.7 55 79.3 5.29
9778 19180.7 55 79.3 5.29
9778 19180.7 55 79.3 5.29
9778 19180.7 55 79.3 5.29
  • 打印进程的 GC 情况
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
$ jstat -gc 22513 1s 5
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
139776.0 139776.0 0.0 0.0 1118528.0 141496.6 2566856.0 30911.3 61292.0 59811.2 7000.0 6588.5 7 0.190 15 0.931 1.121
139776.0 139776.0 0.0 0.0 1118528.0 141496.6 2566856.0 30911.3 61292.0 59811.2 7000.0 6588.5 7 0.190 15 0.931 1.121
139776.0 139776.0 0.0 0.0 1118528.0 141496.6 2566856.0 30911.3 61292.0 59811.2 7000.0 6588.5 7 0.190 15 0.931 1.121
139776.0 139776.0 0.0 0.0 1118528.0 141496.6 2566856.0 30911.3 61292.0 59811.2 7000.0 6588.5 7 0.190 15 0.931 1.121
139776.0 139776.0 0.0 0.0 1118528.0 141496.6 2566856.0 30911.3 61292.0 59811.2 7000.0 6588.5 7 0.190 15 0.931 1.121

$ jstat -gccapacity 22513 1s 5
NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC
1398080.0 1398080.0 1398080.0 139776.0 139776.0 1118528.0 699072.0 2796224.0 2566856.0 2566856.0 0.0 1103872.0 61292.0 0.0 1048576.0 7000.0 7 15
1398080.0 1398080.0 1398080.0 139776.0 139776.0 1118528.0 699072.0 2796224.0 2566856.0 2566856.0 0.0 1103872.0 61292.0 0.0 1048576.0 7000.0 7 15
1398080.0 1398080.0 1398080.0 139776.0 139776.0 1118528.0 699072.0 2796224.0 2566856.0 2566856.0 0.0 1103872.0 61292.0 0.0 1048576.0 7000.0 7 15
1398080.0 1398080.0 1398080.0 139776.0 139776.0 1118528.0 699072.0 2796224.0 2566856.0 2566856.0 0.0 1103872.0 61292.0 0.0 1048576.0 7000.0 7 15
1398080.0 1398080.0 1398080.0 139776.0 139776.0 1118528.0 699072.0 2796224.0 2566856.0 2566856.0 0.0 1103872.0 61292.0 0.0 1048576.0 7000.0 7 15

$ jstat -gcutil 22513 1s 5
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 12.65 1.20 97.58 94.12 7 0.190 15 0.931 1.121
0.00 0.00 12.65 1.20 97.58 94.12 7 0.190 15 0.931 1.121
0.00 0.00 12.65 1.20 97.58 94.12 7 0.190 15 0.931 1.121
0.00 0.00 12.65 1.20 97.58 94.12 7 0.190 15 0.931 1.121
0.00 0.00 12.65 1.20 97.58 94.12 7 0.190 15 0.931 1.121

$ jstat -gccause 22513
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
0.00 0.00 13.42 1.20 97.58 94.12 7 0.190 15 0.931 1.121 CMS Final Remark No GC

监控项说明:

变量 说明 单位
S0 年轻代中第 1 个 survivor 已使用容量占比 百分比
S1 年轻代中第 2 个 survivor 已使用容量占比 百分比
S0C 年轻代中第 1 个 survivor 的容量 字节
S1C 年轻代中第 2 个 survivor 的容量 字节
S0U 年轻代中第 1 个 survivor 已使用容量 字节
S1U 年轻代中第 2 个 survivor 已使用容量 字节
S0CMX 年轻代中第 1 个 survivor 的最大容量 字节
S1CMX 年轻代中第 2 个 survivor 的最大容量 字节
EC 年轻代中 Eden 的容量 字节
EU 年轻代中 Eden 目前已使用容量 字节
OC 老年代的容量 字节
OU 老年代已使用容量 字节
PC 永久代的容量 字节
PU 永久代已使用容量 字节
YGC 从应用程序启动到采样时年轻代中 GC 次数
YGCT 从应用程序启动到采样时年轻代中 GC 所用时间
FGC 从应用程序启动到采样时老年代 GC 次数
FGCT 从应用程序启动到采样时老年代 GC 所用时间
GCT 从应用程序启动到采样时 GC 所用总时间
NGC 年轻代当前容量 字节
NGCMN 年轻代初始容量 字节
NGCMX 年轻代最大容量 字节
OGC 老年代当前容量 字节
OGCMN 老年代初始容量 字节
OGCMX 老年代最大容量 字节
PGC 永久代当前容量 字节
PGCMN 永久代初始容量 字节
PGCMX 永久代最大容量 字节
E 年轻代中 Eden 已使用容量占比 百分比
O 老年代已使用容量占比 百分比
P 永久代已使用容量占比 百分比
ECMX 年轻代中 Eden 的最大容量 字节
DSS 当前所需 survivor 容量,此时 Eden 区已满 字节
TT s 持有次数限制
MTT s 最大持有次数限制

jinfo

命令 jinfo 用于实时查看和调整虚拟机的各项参数,相对于 jps -v 仅显示显式设置的 JVM 启动参数而言,能够显示一些默认的 JVM 启动参数。

1
2
3
4
5
jinfo [option] pid

参数:
-flag [参数名]: 查询和调整参数值
-sysprops: 打印 System.getProperties 的环境变量值

可以通过 jinfo -flag [+|-] namejinfo -flag name=value 修改一部分运行时可写的 JVM 参数,需要注意的是 jinfo 在一些平台下功能受限。

需要注意的是,在 java 8 中,jinfo 虽然可以更改任一标志的值,但并不意味着 JVM 会响应这些更改。例如,大多数影响 GC 算法行为的标志会在启动时决定垃圾回收器的行为方式,之后通过 jinfo 更改这些标志并不会改变 JVM 的行为,JVM 会根据初始的算法继续执行。所以这个方法只对 PrintFlagsFinal 命令的输出结果中标记为 manageable 的标志起作用。在 java 11 中,jinfo 会在你尝试修改不可变标志的值时报告错误。

jmap & jhat

命令 jmap 主要用于生成堆转储快照(通常所说的 heapdump 或 dump 文件),此外还可用于查询 finalize 执行队列,java 堆和方法区的详细信息(空间使用率、收集器类型等)。

1
2
3
4
5
6
7
8
jmap [参数] vmid

-dump:生成 java 堆转储快照,格式:jmap -dump:[live,] format=b, file=<filename.hprof>,其中 live 参数用于指定只 dump 出存活的对象
-finalizerinfo: 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象
-heap: 显示 java 堆的详细信息(收集器类型、参数配置、分代状况等)
-histo: 显示堆中对象统计信息,包括类、实例数、合计容量
-clstats: 以 ClassLoader 为统计口径,显式类加载统计信息
-F: 当虚拟机对 -dump 参数没有响应时,可以使用该选项强制生成 dump 快照

显式 java 堆的详细信息:

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
$ jmap -heap 22513
Attaching to process ID 22513, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.202-b08

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 1431633920 (1365.3125MB)
MaxNewSize = 1431633920 (1365.3125MB)
OldSize = 715849728 (682.6875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 1288503296 (1228.8125MB)
used = 678135408 (646.7203216552734MB)
free = 610367888 (582.0921783447266MB)
52.629699132721505% used
Eden Space:
capacity = 1145372672 (1092.3125MB)
used = 678135408 (646.7203216552734MB)
free = 467237264 (445.59217834472656MB)
59.20652941857513% used
From Space:
capacity = 143130624 (136.5MB)
used = 0 (0.0MB)
free = 143130624 (136.5MB)
0.0% used
To Space:
capacity = 143130624 (136.5MB)
used = 0 (0.0MB)
free = 143130624 (136.5MB)
0.0% used
concurrent mark-sweep generation: # 老年代使用情况
capacity = 2628460544 (2506.6953125MB)
used = 31653136 (30.186782836914062MB)
free = 2596807408 (2476.508529663086MB)
1.2042461916445644% used

22244 interned Strings occupying 2251000 bytes.

由于 jmap 需要访问堆中的所有对象,为了保证整个过程中不被应用线程所干扰,jmap 需要借助安全点机制让所有线程暂停以不改变堆中数据的状态,这可能导致基于该堆转储快照分析出的结果存在偏差。此外,如果某个线程长时间无法运行至安全点,jmap 命令将一直等下去。

命令 jhat 提供了对 jmap 生成的堆转储快照的分析支持,但是一般情况下不推荐使用该命令,通常的做法都是将转储文件拷贝到本地,借助可视化工具(比如:VisualVMMAT: Eclipse Memory AnalyzerIBM HeapAnalyzer 等)进行分析。

jstack

命令 jstack 用于生成虚拟机当前的线程转储快照(通常所说的 threaddump 文件或 javacore 文件),包含当前虚拟机内每个条线程正在执行的方法的堆栈信息,从而分析程序当前是否存在死锁、死循环,以及阻塞等情况。

1
2
3
4
5
6
jstack [参数] vmid

参数说明:
-F: 当正常的输出请求不被响应时,强制输出线程堆栈
-l: 除堆栈外,显示关于锁的附加信息
-m: 如果调用的是本地方法,可以显示 C/C++ 的堆栈

从 JDK 5 起,Thread 类新增了 Thread#getAllStackTraces 方法以获取虚拟机中所有线程的 StackTraceElement 对象,可以基于该方法以程序的方式实现 jstack 命令的大部分功能。

下面我们来写一个死锁程序,并通过 jstack 命令输出程序在死锁时的堆栈信息:

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
private static class A {
}

private static class B {
}

public static void main(String[] args) {
A a = new A();
B b = new B();

final Thread thread1 = new Thread(() -> {
synchronized (a) {
sleep(10); // 暂停一下,给其它线程获取锁的时间
synchronized (b) {
System.out.println("thread: " + Thread.currentThread().getName());
}
}
}, "1");

final Thread thread2 = new Thread(() -> {
synchronized (b) {
sleep(10); // 暂停一下,给其它线程获取锁的时间
synchronized (a) {
System.out.println("thread: " + Thread.currentThread().getName());
}
}
}, "2");

thread1.start();
thread2.start();
}

执行 jstack 命令输出堆栈信息:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
$ jstack 16844
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):

"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x00000000032c3000 nid=0x3e90 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"2" #13 prio=5 os_prio=0 tid=0x00000000202b9800 nid=0x3d60 waiting for monitor entry [0x0000000020c2f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.zhenchao.jvm.DeadLock.lambda$main$1(DeadLock.java:32)
- waiting to lock <0x000000076be2a648> (a org.zhenchao.jvm.DeadLock$A)
- locked <0x000000076be2c9e8> (a org.zhenchao.jvm.DeadLock$B)
at org.zhenchao.jvm.DeadLock$$Lambda$2/999966131.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

"1" #12 prio=5 os_prio=0 tid=0x00000000202b9000 nid=0x10dc waiting for monitor entry [0x0000000020b2f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.zhenchao.jvm.DeadLock.lambda$main$0(DeadLock.java:23)
- waiting to lock <0x000000076be2c9e8> (a org.zhenchao.jvm.DeadLock$B)
- locked <0x000000076be2a648> (a org.zhenchao.jvm.DeadLock$A)
at org.zhenchao.jvm.DeadLock$$Lambda$1/2093631819.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001e766000 nid=0x8b4 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

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

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

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

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

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001e6b5000 nid=0x3120 runnable [0x000000001fc2e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076bd049c0> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076bd049c0> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

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

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

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001cf13000 nid=0x3ec4 in Object.wait() [0x000000001f92f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076bb88ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076bb88ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001e5d3000 nid=0x25a0 in Object.wait() [0x000000001f82e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076bb86c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076bb86c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x000000001cf07000 nid=0x4278 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000032d8800 nid=0x3fbc runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000032da000 nid=0x4174 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000032db800 nid=0x3028 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000032dd000 nid=0x25e8 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000032e0800 nid=0x3edc runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000032e1800 nid=0x43f8 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000032e5000 nid=0x2678 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000032e6000 nid=0x1a8c runnable

"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x00000000032e7000 nid=0x3f30 runnable

"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x00000000032e8800 nid=0x24b0 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001e77c800 nid=0x15c0 waiting on condition

JNI global references: 316


Found one Java-level deadlock:
=============================
"2":
waiting to lock monitor 0x000000001cf12688 (object 0x000000076be2a648, a org.zhenchao.jvm.DeadLock$A),
which is held by "1"
"1":
waiting to lock monitor 0x000000001cf111e8 (object 0x000000076be2c9e8, a org.zhenchao.jvm.DeadLock$B),
which is held by "2"

Java stack information for the threads listed above:
===================================================
"2":
at org.zhenchao.jvm.DeadLock.lambda$main$1(DeadLock.java:32)
- waiting to lock <0x000000076be2a648> (a org.zhenchao.jvm.DeadLock$A)
- locked <0x000000076be2c9e8> (a org.zhenchao.jvm.DeadLock$B)
at org.zhenchao.jvm.DeadLock$$Lambda$2/999966131.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"1":
at org.zhenchao.jvm.DeadLock.lambda$main$0(DeadLock.java:23)
- waiting to lock <0x000000076be2c9e8> (a org.zhenchao.jvm.DeadLock$B)
- locked <0x000000076be2a648> (a org.zhenchao.jvm.DeadLock$A)
at org.zhenchao.jvm.DeadLock$$Lambda$1/2093631819.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

由堆栈信息可以清楚的看到两个线程都在等待获取锁(处于 BLOCKED 状态),并形成了死锁状态。

常用命令

  • java -XX:+PrintFlagsFinal -version:查看当前 JVM 的运行参数设置。
  • jps -l:查看 JVM 进程和对应的驱动类。
  • jstat -gc <pid> <间隔时间>:查看指定 JVM 进程的 GC 情况。
  • jstack -l <pid> > <filename>:生成指定 JVM 的栈内存快照转储文件。
  • jmap -heap <pid>:查看堆内存的使用情况,包括新生代(Eden、From 和 To)和老年代。
  • jmap -histo <pid>:查看堆内存中对象的统计信息,包括对象所属类全限定名、实例数、空间占用等。如果添加 live 后缀(jmap -histo:live <pid>)表示仅显示存活的对象,此时会手动触发一次 Full GC。
  • jmap -dump, format=b, file=<filename>.hprof <pid>:Dump 出指定 JVM 进程的堆内存快照转储,同样可以添加 live 后缀,表示只保留存活的对象,会触发一次 Full GC。

参数调优

任何时候希望为 JVM 设置启动参数之前,建议执行 java -XX:+PrintFlagsFinal -version 查看当前的 JVM 的版本,以及对应参数的默认值,确保目标参数是当前版本 JVM 所支持的,并且默认没有启用。示例:

1
2
3
4
5
6
7
8
$ java -XX:+PrintFlagsFinal -version
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
intx CICompilerCount := 4 {product}

一些说明:

  • =:= 的区别:冒号 := 表示标志使用了非默认值,而 : 表示当前的值是这个版本 JVM 的默认值。
  • 一些标志的默认值在不同平台上可能不一样,通过最后一列可判定:
    • product:表示该标志的默认设置在所有平台是统一的。
    • pd product:表示该标志的默认值依赖于平台。
    • manageable:表示标志的值可以在运行期间动态更改。
    • C2 diagnostic:表示标志可以为编译器工程师提供诊断输出,以了解编译器如何运行。

参考

  1. Java 虚拟机规范(Java SE 8 版)
  2. 深入理解 java 虚拟机(第 2 版)
  3. 深入理解 java 虚拟机(第 3 版)
  4. 极客时间:深入拆解 java 虚拟机
  5. 关键系统的JVM参数推荐(2018仲夏版)