从 jps 到 jmap:JDK 诊断工具的一条排查路线
最近把 java-snippets 里的 JDK 诊断 Demo 补齐了,想按现场排查的顺序走一遍,而不是按字母表背命令。在这里记录一下。环境是 JDK 21。手册和源码都在仓库里1。
一、先起一个常驻进程
排查的第一步是有一个还活着的 JVM。仓库里的「JvmToolkitDemo」只做一件事:后台线程不断分配短生命周期的 byte[],不往集合里塞,用来触发 Young GC。
启动如下:
$ mvn -q compile -DskipTests
$ java -Xms64m -Xmx128m -XX:+UseG1GC \
-Dtoolkit.demo.name=JvmToolkitDemo \
-cp target/classes io.weli.concurrent.JvmToolkitDemo
控制台会打出 PID。分配循环在这里:
// src/main/java/io/weli/concurrent/JvmToolkitDemo.java: 42-54
private static void allocateYoungGarbage() {
long round = 0;
while (true) {
for (int i = 0; i < YOUNG_ALLOCS_PER_ROUND; i++) {
byte[] chunk = new byte[YOUNG_CHUNK_BYTES];
allocationSink = chunk[0];
}
round++;
if (round % 50 == 0) {
System.out.printf("young-alloc round %d%n", round);
}
sleepQuietly(50);
}
}
对应源码:JvmToolkitDemo.java。allocationSink 是 volatile,避免 JIT 把分配整段消掉。
另开一个终端,后面的命令都打在那边。Demo 这个窗口不要关。
二、jps:先确认打的是这个进程
本机同时跑着好几个 Java 时,PID 抓错后面全白做。jps 只列 HotSpot 进程2:
$ jps -l | grep JvmToolkitDemo
65237 io.weli.concurrent.JvmToolkitDemo
$ jps -lvm | grep JvmToolkitDemo
65237 io.weli.concurrent.JvmToolkitDemo -Xms64m -Xmx128m -XX:+UseG1GC -Dtoolkit.demo.name=JvmToolkitDemo
可以看到,-l 给出主类全名,-v 把这次真正带上的 -Xmx、-D 也打出来。jps 自己会出现在列表里,忽略即可。
三、jcmd / jinfo:核对启动参数
不要靠记忆「当时是怎么启动的」。jcmd 是 JDK 7 起的统一入口3,先问这个进程支持哪些命令:
$ jcmd 65237 help
本机接着跑了这几条:
$ jcmd 65237 VM.command_line
$ jcmd 65237 VM.flags
$ jcmd 65237 VM.system_properties | grep toolkit.demo
$ jinfo -flag MaxHeapSize 65237
VM.command_line 的输出如下:
VM Arguments:
jvm_args: -Xms64m -Xmx128m -XX:+UseG1GC -Dtoolkit.demo.name=JvmToolkitDemo
java_command: io.weli.concurrent.JvmToolkitDemo
java_class_path (initial): target/classes
jinfo -flag MaxHeapSize 打出来是 134217728,也就是 128MB,和 -Xmx128m 对得上4。系统属性里能看到 toolkit.demo.name=JvmToolkitDemo。
JDK 21 上我更常用 jcmd VM.*。jinfo 还在,现场同事提起它时对照一下即可。
四、jstat:看接下来几十秒 GC 怎么走
参数核对完,下一个问题是:堆是在正常回收,还是 Old 只增不减。jstat 做周期性采样,不 dump 堆5:
$ jstat -gcutil 65237 1000 5
本机 5 秒里的输出是:
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
- 45.54 28.21 3.89 75.32 34.18 8 0.006 0 0.000 0 0.000 0.006
- 44.15 30.77 3.89 75.32 34.18 9 0.007 0 0.000 0 0.000 0.007
- 43.28 41.03 3.89 75.32 34.18 10 0.009 0 0.000 0 0.000 0.009
- 42.22 43.59 3.89 75.32 34.18 11 0.010 0 0.000 0 0.000 0.010
- 42.70 53.85 3.89 75.32 34.18 12 0.011 0 0.000 0 0.000 0.011
E(Eden)在涨,YGC 从 8 走到 12,O(Old)停在 3.89,FGC 一直是 0。G1 下 S0 常显示为 -,以表头为准。
如上所示,这个 Demo 在分配,但对象活不过 Young 代。堆摘要可以用 jcmd 65237 GC.heap_info 再对一眼。
五、线程问题换 JstackTroubleDemo
CPU 高、卡住、怀疑死锁时,走 jstack(或 jcmd Thread.print)6。先把 Toolkit Demo 停掉,换这个:
$ java -cp target/classes io.weli.concurrent.JstackTroubleDemo
它同时造了三类现场:交叉加锁、占着锁 sleep、空转循环。死锁那段如下:
// src/main/java/io/weli/concurrent/JstackTroubleDemo.java: 31-50
private static void startDeadlock() {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
sleepQuietly(100);
synchronized (LOCK_B) { ... }
}
}, "deadlock-thread-1");
// thread-2 先 B 后 A
}
对应源码:JstackTroubleDemo.java。
$ jstack <pid> > /tmp/jstack-demo.txt
输出末尾会出现 Found one Java-level deadlock。blocked-thread 是 BLOCKED,对面的 lock-holder 持锁在 sleep;busy-loop-thread 是 RUNNABLE,而且 cpu= 远大于别的线程。连续抓三次、间隔几秒,栈顶不变就可以把忙等和正常短任务分开。
jcmd <pid> Thread.print 和 jstack 等价。
六、内存问题换 JmapHeapDemo
OOM、堆持续涨,用 jmap 看直方图,必要时 dump7。这个 Demo 把 byte[] 和 String 放进 static 集合,只增不减:
// src/main/java/io/weli/concurrent/JmapHeapDemo.java: 18-20
private static final List<byte[]> LEAKED_BUFFERS = new ArrayList<>();
private static final List<String> STRING_LEAK = new ArrayList<>();
对应源码:JmapHeapDemo.java。
$ java -Xmx256m -cp target/classes io.weli.concurrent.JmapHeapDemo
$ jmap -histo <pid> | head -20
跑几轮之后,本机看到 [B(byte[])和 java.lang.String 排在前面,实例数大约按轮次 × 1000 往上走。jmap -histo:live 会先做一次 Full GC 再统计:若 GC 之后这两类还很大,强引用泄漏的嫌疑就大了。
JDK 21 上 jmap -heap 经常直接报错,提示改用 jhsdb jmap。活进程用 jcmd <pid> GC.heap_info 就够。要离线分析:
$ jmap -dump:live,format=b,file=/tmp/jmap-heap-demo.hprof <pid>
dump 会 STW。Eclipse MAT 打开这份 .hprof,Leak Suspects 会指到 JmapHeapDemo 的 static 集合;再看 Path to GC Roots,引用链就是 LEAKED_BUFFERS / STRING_LEAK。仓库里有 MAT 手册和抓堆脚本1。
和上一节的 Toolkit Demo 对照一下:
flowchart LR
A["JvmToolkitDemo"] --> B["短生命周期 byte[]"]
B --> C["YGC 升 / Old 不动"]
D["JmapHeapDemo"] --> E["static List 只增不减"]
E --> F["histo 里 B 和 String 持续涨"]
style C fill:#90EE90,stroke:#333,stroke-width:2px
style F fill:#FF6347,stroke:#333,stroke-width:2px
左边是正常 Young GC,右边才是泄漏形状。两个 Demo 不要同时 grep Demo 混 PID。
七、jcmd 把旧命令收拢
现场还是常听到 jstack、jmap、jinfo。JDK 21 上不少能力已经汇到 jcmd:
| 旧命令 | jcmd |
|---|---|
jstack |
Thread.print |
jmap -histo |
GC.class_histogram |
jmap -heap |
GC.heap_info |
jmap -dump |
GC.heap_dump /abs/path.hprof |
jinfo -flags |
VM.flags |
jinfo -sysprops |
VM.system_properties |
jstat 没有完整等价物,连续采样还是用它。GC.heap_dump 要用绝对路径。
整条路线可以压成下面这张图:
flowchart TB
JPS["jps -lvm 锁定 PID"] --> FLAG["jcmd VM.command_line / VM.flags"]
FLAG --> STAT["jstat -gcutil 看趋势"]
STAT --> Q{症状}
Q -->|卡住 / CPU 高| ST["jstack / Thread.print"]
Q -->|堆涨 / OOM| MP["jmap -histo"]
MP --> DP["dump + MAT"]
ST --> GUI["jconsole 本机看图"]
DP --> GUI
Q -->|attach 失败或 core| SA["jhsdb"]
style JPS fill:#87CEEB,stroke:#333,stroke-width:2px
style STAT fill:#90EE90,stroke:#333,stroke-width:2px
style Q fill:#FFD700,stroke:#333,stroke-width:2px
八、jconsole 和 jhsdb
本机看实时曲线,把 PID 传给 jconsole 即可,不要为了练习打开无认证的远程 JMX8:
$ jconsole 65237
Memory 页上 Eden 是锯齿,Threads 里能看到 young-allocator。JDK 21 不再自带 jvisualvm,深度分析还是 MAT。
jhsdb 是 Serviceability Agent,用来补 jmap -heap,或对 core 做事后分析9。本机(macOS)对活进程跑 jhsdb jmap --heap --pid 65237 时,task_for_pid 直接失败;同一时刻 jcmd GC.heap_info 是成功的。Linux 上这条通常能用。能走 jcmd 就先走 jcmd。
九、收一下
| 现象 | 先跑 |
|---|---|
| 一堆 Java,不知道打谁 | jps -l / jps -lvm |
-Xmx、GC、-D 对不对 |
jcmd VM.command_line |
| Young 狂跑还是 Old 在涨 | jstat -gcutil |
| 死锁 / 阻塞 / 忙等 | jstack |
| 谁占了堆 | jmap -histo,引用链再 MAT |
仓库里分册手册和十日练习顺序在 docs/jdk-toolkit-learning-plan.md。后续如果把 JFR 也串进去,再另写一篇。
References
-
liweinan,
java-snippets— JDK 诊断 Demo 与手册。总计划:docs/jdk-toolkit-learning-plan.md;靶进程JvmToolkitDemo.java。 ↩ ↩2 -
Oracle,《The jps Command》(Java SE 21)。列出目标系统上的 JVM 进程;
-l主类全名或 jar 路径,-v传给 JVM 的参数,-m传给main的参数。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jps.html ↩ -
Oracle,《The jcmd Command》(Java SE 21)。向指定 JVM 发送诊断命令;无参数或
help列出该进程可用命令。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html ↩ -
Oracle,《The jinfo Command》(Java SE 21)。查看或修改 Java 进程的配置。
-flag name打印单个 VM 标志。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jinfo.html ↩ -
Oracle,《The jstat Command》(Java SE 21)。对已插桩的 HotSpot JVM 采样;
-gcutil为各代占用百分比与 GC 计数。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jstat.html ↩ -
Oracle,《The jstack Command》(Java SE 21)。打印 Java 线程的栈跟踪。等价诊断命令为
jcmd <pid> Thread.print。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jstack.html ↩ -
Oracle,《The jmap Command》(Java SE 21)。
-histo对象直方图;-dump:live,format=b,file=导出 hprof。文档写明部分选项在部分环境下不可用,可改用jhsdb jmap。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jmap.html ↩ -
Oracle,《The jconsole Command》(Java SE 21)。JMX 合规的图形监控工具,可连接本地或远程 JVM。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jconsole.html ↩
-
Oracle,《The jhsdb Command》(Java SE 21)。Serviceability Agent;
jmap --heap --pid查看堆配置,亦可对 core 使用--core/--exe。参见:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jhsdb.html ↩