一、工具箱全景:每个工具解决什么问题
JDK 自带了一套强大的诊断工具集,它们在 $JAVA_HOME/bin 目录下,不依赖任何第三方库。但很多工程师只会用 jstack 看线程栈,遇到其他问题就束手无策。本文的目标是让你建立一套"问题 -> 工具 -> 命令 -> 解读"的完整映射。
先给一张全景速查表:
| 工具 | 核心用途 | 一句话描述 |
|---|---|---|
| jps | 进程发现 | 列出当前用户的所有 Java 进程及其主类 |
| jstat | 运行时统计 | 实时监控 GC、类加载、JIT 编译等指标 |
| jstack | 线程分析 | 导出线程栈,诊断死锁、线程阻塞、CPU 飙高 |
| jmap | 内存分析 | 导出堆转储、查看堆直方图、查看类实例 |
| jcmd | 统一诊断 | JDK 8+ 推荐的一体化诊断命令 |
| Arthas | 在线诊断 | 阿里开源,无需重启即可在线排查问题的神器 |
[图1:JVM诊断工具选择决策树——从问题类型出发,引导到正确的工具和命令]

二、jps:知道有哪些 Java 进程在跑
2.1 基础用法
jps # 列出 pid 和主类名
jps -l # 显示完整的主类名或 jar 路径
jps -v # 显示 JVM 启动参数
jps -m # 显示传递给 main 方法的参数
jps -lv # 组合使用
2.2 典型场景
在服务器上排查问题时,第一步通常是 jps -lv,确认目标进程的 pid 和启动参数(特别是 -Xmx、-Xms、GC 收集器等)。如果发现多个同名 Java 进程,通过 -m 查看启动参数来区分。
2.3 原理
jps 通过读取 /tmp/hsperfdata_/ 目录下的临时文件来获取进程信息。如果应用以不同用户身份运行,jps 只能看到当前用户的进程。
三、jstat:实时监控 JVM 运行状态
3.1 核心命令
jstat -gc <pid> 1000 10 # 每秒输出一次 GC 统计,共 10 次
jstat -gcutil <pid> 1000 # 每秒输出 GC 使用率百分比,持续输出
jstat -class <pid> 1000 # 每秒输出类加载统计
jstat -compiler <pid> # JIT 编译统计
jstat -gccapacity <pid> # 各代容量
jstat -gccause <pid> # GC 原因
3.2 -gcutil 输出解读
jstat -gcutil 是最常用的模式,输出各内存区域的使用率百分比:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 45.23 62.17 78.56 92.13 89.45 1234 8.765 5 2.345 11.110
| 列 | 含义 | 关注点 |
|---|---|---|
| S0/S1 | Survivor 0/1 使用率 | 两块交替使用,一块为 0 是正常的 |
| E | Eden 使用率 | 高时即将触发 Minor GC |
| O | Old 使用率 | 持续增长且 FGC 回收少 -> 内存泄漏 |
| M | Metaspace 使用率 | 接近 MaxMetaspaceSize -> 类加载过多 |
| YGC/YGCT | Young GC 次数/总耗时 | 频率高 -> Eden 太小或分配速率高 |
| FGC/FGCT | Full GC 次数/总耗时 | 频繁 -> 老年代容量不足或泄漏 |
| GCT | GC 总耗时 | 占运行时间的比例 |
[图2:jstat -gcutil 输出解读标注图——仿终端风格,用彩色标注框标出各列含义和关注点]

3.3 实战:判断内存泄漏
jstat -gcutil <pid> 2000
如果 O 列在 Full GC 后仍然很高且持续增长,大概率是内存泄漏。如果 O 列随 YGC 周期性波动(Minor GC 后 O 增加),说明晋升速率正常,是容量问题。
四、jstack:线程栈分析
4.1 基础用法
jstack <pid> # 输出线程栈
jstack -l <pid> # 额外输出锁信息
jstack <pid> > thread_dump.txt # 保存到文件
4.2 线程状态解读
jstack 输出中每个线程的 java.lang.Thread.State 是最关键的信息:
| 状态 | 含义 | 常见原因 |
|---|---|---|
| RUNNABLE | 正在执行(可能在等待 CPU) | 正常 / CPU 密集计算 |
| BLOCKED | 等待获取监视器锁 | synchronized 竞争 |
| WAITING | 无限期等待(wait/join/park) | 等待条件满足 |
| TIMED_WAITING | 限时等待(sleep/wait(timeout)) | 超时等待 |
[图3:jstack 线程状态分布图——不同线程状态的柱状图,标注各状态的含义和常见原因]

4.3 实战:CPU 飙高排查
# 1. 找到 CPU 最高的线程
top -H -p <pid>
# 2. 将 tid 转为十六进制
printf "%x" <tid>
# 3. 在 jstack 中搜索该 nid
jstack <pid> | grep -A 20 "nid=0x<hex_tid>"
找到后查看线程栈,定位正在执行的方法。如果大量线程都在执行同一个方法,那就是热点代码。
4.4 实战:死锁检测
jstack -l <pid> | grep -A 50 "deadlock"
# 或者
jcmd <pid> Thread.print -l
如果存在死锁,jstack 输出末尾会明确列出涉及死锁的线程和锁对象。
4.5 常见线程池问题
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -nr
如果大量线程处于 WAITING 状态,说明线程池空闲线程多,可能配置过大。如果大量线程处于 BLOCKED,说明锁竞争严重。
五、jmap:内存分析
5.1 核心命令
# 类直方图(按占用内存排序)
jmap -histo <pid> | head -30
# 只统计存活对象(先触发 Full GC)
jmap -histo:live <pid> | head -30
# 导出堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
jmap -dump:format=b,file=heap_all.hprof <pid>
5.2 jmap -histo 的实战价值
jmap -histo 是排查"哪个类占内存最多"的最快方式,不需要导出 dump 文件,几秒钟就能出结果:
num #instances #bytes class name
1: 5000000 400000000 [C
2: 3000000 72000000 java.lang.String
3: 500000 60000000 [B
4: 100000 40000000 java.util.HashMap$Node
如果某个类的实例数远超预期,直接定位到可能的泄漏点。
[图4:jmap -histo 输出示例标注图——类直方图输出,标注实例数、字节数、类名,标注可能的泄漏嫌疑类]

5.3 注意事项
- jmap -histo:live 会触发 Full GC,生产环境慎用。
- jmap -dump 导出时应用会暂停(STW),时间取决于堆大小。8GB 堆约需 10-30 秒。
- JDK 9+ 推荐用 jcmd 替代 jmap。
六、jcmd:JDK 的统一诊断入口
6.1 为什么用 jcmd
JDK 从 8 开始推荐用 jcmd 替代分散的 jps、jstack、jmap 等工具。jcmd 是一个统一的诊断命令入口。
6.2 核心命令
jcmd <pid> help # 列出该进程支持的所有诊断命令
jcmd <pid> VM.version # JVM 版本信息
jcmd <pid> VM.flags # JVM 启动参数(最终生效值)
jcmd <pid> VM.system_properties # 系统属性
jcmd <pid> VM.uptime # JVM 运行时间
jcmd <pid> Thread.print -l # 线程栈(等同于 jstack -l)
jcmd <pid> GC.heap_dump /path/to/dump.hprof # 堆转储
jcmd <pid> GC.class_histogram # 类直方图
jcmd <pid> GC.run # 触发 GC
jcmd <pid> VM.native_memory summary # 本地内存(需开启 NMT)
6.3 实战:快速查看 JVM 全景
jcmd <pid> VM.flags # 最终生效的 JVM 参数
jcmd <pid> VM.uptime # 运行时间
jcmd <pid> VM.native_memory summary # 本地内存(需 -XX:NativeMemoryTracking=summary)
七、Arthas:线上诊断的瑞士军刀
7.1 为什么需要 Arthas
JDK 自带工具只能做"快照式"诊断。Arthas 提供了交互式、实时、无需重启的诊断能力。
7.2 核心命令速查
# 安装启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 仪表盘(实时 CPU、内存、GC、线程)
dashboard
# 线程
thread # 所有线程
thread -n 3 # CPU 最高的 3 个线程
thread -b # 死锁检测
thread --state BLOCKED # 只看 BLOCKED 状态
# 方法监控
watch com.example.Service processUser "{params, returnObj, throwExp}" -x 3
trace com.example.Service processUser
monitor -c 5 com.example.Service processUser
# 类信息
sc -d com.example.Service # 类详细信息
sm com.example.Service # 方法签名
jad com.example.Service # 在线反编译
# 内存
vmtool --action getInstances --className java.lang.String --limit 10
heapdump /tmp/dump.hprof
# 热更新
retransform /tmp/MyService.class
[图5:Arthas dashboard 界面示意图——仿 Arthas 仪表盘,展示 CPU、内存、GC、线程等实时指标]

7.3 实战:定位接口慢的原因
trace com.example.controller.OrderController createOrder
# 输出示例:
# +---[99.9% 1523.45ms] OrderService:createOrder()
# +---[80.2% 1222.34ms] InventoryService:deductStock()
# +---[98.1% 1199.12ms] Database:update()
一眼看出 80% 的时间花在库存扣减的数据库操作上,直接定位优化方向。
7.4 实战:热修复线上 Bug
# 1. 反编译当前类
jad --source-only com.example.MyService > /tmp/MyService.java
# 2. 修改源代码
# 3. 编译
javac -cp ... /tmp/MyService.java
# 4. 热更新
retransform /tmp/MyService.class
注意:热更新不支持新增字段和方法,只能修改方法体。重启后失效,必须同步修改源码。
八、Arthas 的限制与最佳实践
8.1 限制
- 性能开销:watch 和 trace 对目标方法有性能影响,不要在生产高峰期对高频方法使用。
- 热更新限制:不能新增/删除字段和方法,不能修改类层次结构。
- 安全风险:Arthas 本质上是 JVM Agent,可以执行任意代码,生产环境需要严格权限控制。
8.2 最佳实践
- 生产环境优先用 jstat、jcmd 等轻量级 JDK 工具做初步诊断。
- 需要深入分析时再用 Arthas,用完立即 stop 退出。
- watch 和 trace 加 -n 限制输出次数,避免日志爆炸。
- 热更新后务必同步修改源码,并在下次发版时正式修复。
九、总结
各工具的定位和优先级:
- 日常监控:jstat -gcutil 是性价比最高的监控命令。
- CPU 问题:top -H + jstack 组合拳,定位热点线程和代码。
- 内存问题:jmap -histo 快速看类分布 -> jmap -dump 导出 dump -> MAT 分析。
- 线程问题:jstack 看状态分布和死锁 -> jcmd Thread.print -l 做补充。
- 在线调试:Arthas 的 watch、trace、dashboard。
- 统一入口:jcmd 是趋势,新项目尽量用 jcmd 替代分散命令。
[图6:六种工具对比速查表——jps/jstat/jstack/jmap/jcmd/Arthas 的对比,列出核心命令、典型场景、适用环境]

记住:工具只是手段,关键是建立"问题 -> 假设 -> 验证"的排查思路。先看症状(CPU 高?内存高?响应慢?),再选工具,而不是拿着工具到处试。
延伸阅读
- JDK 官方文档:jstat、jstack、jmap、jcmd 手册
- Arthas 官方文档:https://arthas.aliyun.com/doc/
- 本系列相关篇目:064(常见 OOM 类型)、065(内存泄漏分析)、067(GC 日志分析)
面试官视角:提问与回答
以下问题从面试官视角出发,考察你对 JVM 诊断工具的实战经验和深度理解。
Q1:线上服务 CPU 飙到 100%,你登上服务器后发现 jstack 执行了 30 秒还没出结果。这时你会怎么做?
jstack 执行慢通常说明 JVM 本身处于高负载或 GC 频繁状态。我会先尝试以下顺序:1)用 top -H -p 查看哪个线程 CPU 最高,记录 nid。2)如果 jstack 太慢,改用 jcmd Thread.print -l,它通常比 jstack 更快。3)如果 jcmd 也慢,说明 JVM 可能在频繁 Full GC 或 Safepoint 等待,先用 jstat -gcutil 看 GC 状态。4)如果 GC 正常,用 Arthas 的 thread -n 3 快速查看 CPU 最高的线程,Arthas 的 attach 机制有时能绕过 jstack 的阻塞。关键是不要死等一个工具,要并行尝试多个入口。
Q2:你用 jmap -histo 发现内存里占用最多的是 char[] 和 String,但手工检查代码没发现明显的字符串滥用。你接下来怎么排查?
char[] 和 String 占内存最大是正常的(它们是 Java 应用中最常见的对象类型),关键是看它们的来源。我会:1)导出完整 dump(不加 :live),用 MAT 打开。2)在 Dominator Tree 中展开 char[] 或 String 的支配者,看是哪个对象持有了最多 String。3)通常持有大量 String 的是 HashMap、ArrayList、或是某个缓存框架的内部数据结构。4)如果发现是 Jackson/Gson 的 JsonNode 对象持有大量 String,可能是反序列化时保留了完整 JSON 树而非流式解析。5)如果发现是日志框架的 LogEvent 持有大量 String,可能是日志量过大或异步日志队列积压。String 本身不是问题,问题在于谁持有它们以及为什么持有这么久。
Q3:Arthas 的 trace 命令很方便,但你在生产环境对一个高频方法(每秒调用 1000 次)执行 trace 时,需要注意什么风险?
高频方法上执行 trace 有三个风险:1)性能开销:trace 会对目标方法做字节码增强,每次调用都要记录入参、返回值和时间戳,对 1000 QPS 的方法可能增加 10-30% 的 CPU 开销。2)日志爆炸:每次调用都输出完整的调用链,控制台刷屏且可能撑爆 Arthas 的内存。3)Safepoint 影响:字节码增强需要进入 Safepoint,高频方法可能增加 Safepoint 等待时间。正确的做法:加 -n 限制输出次数(如 trace ... -n 10),或者在高频方法上先看 monitor 统计,确认有问题再用 trace 采样。
Q4:Docker 容器里没有 JDK,只有 JRE。你有哪些工具还能用?怎么排查问题?
只有 JRE 的环境缺失了 jps、jstat、jstack、jmap 等工具。应对方案:1)如果容器支持,临时拷贝 JDK 的 tools.jar 和相应二进制文件进去。2)用 docker exec 从宿主机执行 jcmd(需要同版本 JDK 且能访问容器内的进程)。3)在应用启动参数中加入 -XX:+HeapDumpOnOutOfMemoryError 和 JMX 配置,通过 JMX 获取运行时信息。4)最佳实践是容器镜像中始终包含 JDK 而非 JRE——对于排查问题的价值远大于多出的几百 MB 镜像大小。5)如果以上都不行,只能靠应用自身的监控埋点(如 Micrometer + Prometheus)和日志来判断。
Q5:你在一个服务上同时看到 jstack 显示大量线程 BLOCKED,jstat 显示频繁 FGC,Arthas trace 显示某个方法的数据库调用耗时 10 秒。你认为这三个现象之间有关联吗?排查的切入点应该是什么?
高度关联,典型的"数据库慢查询引发连锁故障": 数据库慢查询(10 秒耗时)-> 大量请求线程堆积在数据库连接获取上(BLOCKED 在数据库连接池的 getConnection)-> 请求堆积导致内存中临时对象累积 -> 触发频繁 Full GC -> 进一步降低吞吐量 -> 恶性循环。排查切入点:先在 Arthas 中确认是哪个 SQL 慢、为什么慢(缺索引?锁等待?全表扫描?),解决数据库层面的问题后,线程 BLOCKED 和 FGC 通常会自然缓解。如果先去看 FGC 或线程 BLOCKED,容易被表面现象误导——它们是"受害者"而非"肇事者"。