本节摘要:四件套覆盖标准体检的全部动作:jstat 看动态水位与 GC 节奏,jmap 看堆内构成与快照,jstack 看线程在忙什么,jcmd 是万能瑞士刀。本节按"先动态后静态"的顺序过一遍每件工具的常用姿势与输出读法,并给出体检顺序建议——这一节是第六章后续所有病案的手脚。
体检之前先立指标框架,否则看了数字也不知道看什么。Java 服务的核心体检指标就四组:内存水位(堆各代与堆外的占用与趋势)、GC 节奏(频率、停顿、净收益,第五章三条曲线)、线程状态(多少在跑、多少被堵、有没有死锁)、以及热点(CPU 与方法级热点)。四件套正好分工:jstat 管前两组的动态观测,jmap 管内存构成,jstack 管线程,jcmd 全都能干。
jstat 的价值是"连续观察而不惊动进程"——不抓快照、不产生大对象拷贝,适合作为第一步。先找进程,再看水位:
$ jps -l 29104 com.demo.OrderService $ jstat -gcutil 29104 1000 5 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 98.44 62.10 41.35 95.12 90.02 118 1.842 2 0.312 2.154 0.00 98.44 88.07 41.35 95.12 90.02 118 1.842 2 0.312 2.154 47.31 0.00 5.22 41.63 95.12 90.02 119 1.856 2 0.312 2.168 0.00 51.87 46.75 41.63 95.12 90.02 120 1.869 2 0.312 2.181 0.00 74.09 71.33 41.63 95.12 90.02 120 1.869 2 0.312 2.181
-gcutil 输出各区域的占用百分比:S0、S1 是两块 Survivor,E 是 Eden,O 是老年代,M 是 Metaspace 使用率。读法完全沿用第五章的等式:看 E 涨落的节奏(分配速率)、O 的趋势(晋升与泄漏)、YGC 与 FGC 的次数增量(回收频率)、GCT 的增量(每秒被 GC 吃掉多少时间)。上面这份采样是健康的:年轻回收规律发生、老年代几乎不动、FGC 没有增长。
想看绝对值换 -gc,第一列到最后一列是各区容量与已用(第二章 2.1 的字段对照表就是为它准备的)。观察节奏选采样间隔:秒级看波动,分钟级看趋势,两个都要看。
水位异常后,用 jmap 看堆里到底装了什么。第一招直方图,按类汇总实例数与内存:
$ jmap -histo 29104 | head -n 12 num #instances #bytes class name 1: 2104532 50508768 byte[] 2: 612340 24593600 java.util.HashMap$Node 3: 420011 13440352 java.util.concurrent.ConcurrentHashMap$Node 4: 88204 4233792 java.lang.String 5: 3312 2206608 byte[] (value)
读直方图的诀窍是"对不上号的类最可疑":byte[] 与基础容器类的大头通常有业务解释(缓存了大对象、大结果集),而某个业务类实例数离谱(比如上百万个 OrderItem 还在涨),基本就是泄漏嫌疑人。标准动作是隔一段时间抓两份对比,涨得快的那类就是追查目标(6.3 病案一实演)。
第二招堆快照,用于深挖引用链:
$ jmap -dump:live,format=b,file=heap.hprof 29104 Dumping heap to heap.hprof ... Heap dump file created [89123456 bytes in 3.214 secs]
live 选项会先触发一次 Full GC 再转储——这正是它该谨慎用于生产的原因:大堆服务一次全停顿可能秒级,引发超时雪崩。规矩是:能先用直方图就先用;确要 dump,先摘流量再动手;文件别落在系统盘。快照用 MAT 或 JProfiler 打开,沿着"支配树与引用链"找到 Roots 持有者。
第三招看各类占用汇总与 finalizer 队列:jmap -clstats 看类加载器统计(Metaspace 泄漏时用它对加载器对账)。
线程体检一张快照全带走:
$ jstack 29104 | head -n 20 2024-03-02 11:42:07 Full thread dump OpenJDK 64-Bit Server VM (17.0.10+7): "http-nio-8080-exec-12" #34 daemon prio=5 os_prio=0 cpu=1240.11ms elapsed=421.90s java.lang.Thread.State: RUNNABLE at com.demo.rule.RuleEngine.eval(RuleEngine.java:88) at com.demo.rule.RuleChain.fire(RuleChain.java:41) ... "http-nio-8080-exec-7" #29 daemon prio=5 os_prio=0 cpu=88.15ms elapsed=421.90s java.lang.Thread.State: BLOCKED (on object monitor) at com.demo.cache.LocalCache.get(LocalCache.java:57) - waiting on the object monitor <0x000000076ab62208> ...
每个线程一段:名字、状态、调用栈。判读口诀按状态走——大量 RUNNABLE 的线程挤在同一行业务代码,多半是 CPU 热点(对号入座法在 6.3 病案四);大量 BLOCKED 停在同一把锁上,是锁竞争(病案三);WAITING 看等的是谁;文件末尾如果有 "Found one Java-level deadlock",机器直接把死锁环给你画出来了,连查都不用查。线程池的排除也靠它:名字带 pool-N-thread-M 却长期 WAITING 的池,可能是配大了空转;任务堆积的池看队列线程是否都在 BLOCKED。
单次快照会骗人(恰好赶上瞬时状态),连抓几份隔几秒的快照对比,"持续 BLOCKED 在同一行"才算实锤。
jcmd 把上面几件整合并扩展,语法是"进程号加命令":
$ jcmd 29104 VM.flags # 查看生效的全部 JVM 参数 $ jcmd 29104 GC.heap_info # 堆概况(收集器视角) $ jcmd 29104 Thread.print # 等价 jstack $ jcmd 29104 GC.class_histogram # 等价 jmap -histo $ jcmd 29104 GC.run # 请求一次 GC(诊断用,勿常规化) $ jcmd 29104 VM.uptime # 起来多久了
生产上推荐优先用 jcmd:它是官方维护的入口(同一命令不同版本行为稳定),还能动态开 JFR(下一节的主角)。jmap 与 jstack 保留作为最低依赖路径——极端情况下(进程僵死、版本老旧)jcmd 可能无法附加,老工具反而更皮实。
⚠️ 常见坑:附加失败第一反应去重装 JDK。Windows 上 jstack 老版本需要与目标进程同位数、同用户;容器里工具进程与目标进程的 PID 命名空间、镜像里的 JDK 是否含诊断模块,都会导致附加失败。先检查环境一致性,再怀疑工具本身。
💡 关键直觉:体检的动作顺序本身就是诊断逻辑——jstat 定位"哪类指标异常",jmap 与 jstack 对异常指标取证,jcmd 收尾汇总。顺序对了,四件套个个省心;顺序错了(上来就 dump 大堆),体检本身就会变成事故。
四件套解决"此刻怎么样",但有些问题要"回放之前发生了什么"——下一节的 JFR 连续记录与 Async-Profiler 火焰图,把体检从快照升级为录像。