JVM 常用诊断工具:jps、jstat、jstack、jmap、jcmd、Arthas(066)

JVM原理与调优约 9 分钟阅读5,103 字符
知识库文档 67 / 251

一、工具箱全景:每个工具解决什么问题

JDK 自带了一套强大的诊断工具集,它们在 $JAVA_HOME/bin 目录下,不依赖任何第三方库。但很多工程师只会用 jstack 看线程栈,遇到其他问题就束手无策。本文的目标是让你建立一套"问题 -> 工具 -> 命令 -> 解读"的完整映射。

先给一张全景速查表:

工具 核心用途 一句话描述
jps 进程发现 列出当前用户的所有 Java 进程及其主类
jstat 运行时统计 实时监控 GC、类加载、JIT 编译等指标
jstack 线程分析 导出线程栈,诊断死锁、线程阻塞、CPU 飙高
jmap 内存分析 导出堆转储、查看堆直方图、查看类实例
jcmd 统一诊断 JDK 8+ 推荐的一体化诊断命令
Arthas 在线诊断 阿里开源,无需重启即可在线排查问题的神器

[图1:JVM诊断工具选择决策树——从问题类型出发,引导到正确的工具和命令]

文章配图
文章配图

二、jps:知道有哪些 Java 进程在跑

2.1 基础用法

bash
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 核心命令

bash
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 是最常用的模式,输出各内存区域的使用率百分比:

code
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 实战:判断内存泄漏

bash
jstat -gcutil <pid> 2000

如果 O 列在 Full GC 后仍然很高且持续增长,大概率是内存泄漏。如果 O 列随 YGC 周期性波动(Minor GC 后 O 增加),说明晋升速率正常,是容量问题。

四、jstack:线程栈分析

4.1 基础用法

bash
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 飙高排查

bash
# 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 实战:死锁检测

bash
jstack -l <pid> | grep -A 50 "deadlock"
# 或者
jcmd <pid> Thread.print -l

如果存在死锁,jstack 输出末尾会明确列出涉及死锁的线程和锁对象。

4.5 常见线程池问题

bash
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -nr

如果大量线程处于 WAITING 状态,说明线程池空闲线程多,可能配置过大。如果大量线程处于 BLOCKED,说明锁竞争严重。

五、jmap:内存分析

5.1 核心命令

bash
# 类直方图(按占用内存排序)
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 文件,几秒钟就能出结果:

code
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 核心命令

bash
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 全景

bash
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 核心命令速查

bash
# 安装启动
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 实战:定位接口慢的原因

bash
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

bash
# 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 限制输出次数,避免日志爆炸。
  • 热更新后务必同步修改源码,并在下次发版时正式修复。

九、总结

各工具的定位和优先级:

  1. 日常监控:jstat -gcutil 是性价比最高的监控命令。
  2. CPU 问题:top -H + jstack 组合拳,定位热点线程和代码。
  3. 内存问题:jmap -histo 快速看类分布 -> jmap -dump 导出 dump -> MAT 分析。
  4. 线程问题:jstack 看状态分布和死锁 -> jcmd Thread.print -l 做补充。
  5. 在线调试:Arthas 的 watch、trace、dashboard。
  6. 统一入口: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,容易被表面现象误导——它们是"受害者"而非"肇事者"。