跳到主要内容
约 8 分钟3 张卡

JVM 诊断

JVM 诊断通过运行数据定位进程中的性能与稳定性问题。本文先说明怎样建立故障时间线,再按症状选择线程转储、JFR 和采样工具。最后介绍采集动作的影响,以及如何记录现场信息,便于把不同证据放在一起分析。

1. 面试核心​

先记住这几条,再读正文
  • 转储类型。 线程转储记录某一时刻线程执行到哪里、在等待什么,堆转储记录对象之间的引用关系。卡 · 故障时间线
  • 时间线关联。 GC 日志记录回收时间线,JFR 将 CPU、分配、锁和编译等运行事件关联到同一时间轴。
  • 连续采样。 间隔几秒采集 3—5 份线程转储,可以区分线程持续停留在某处和偶然经过某处。卡 · 快照与时间序列
  • 堆转储影响。 JDK 21 的 GC.heap_dump 默认请求 Full GC,使用 -all 省去该请求后仍需要遍历堆并写出文件。卡 · 堆转储与显式回收

2. 故障时间线​

诊断前先写清用户遇到了什么问题,以及问题何时发生。这样才能判断采集到的等待、大对象或 GC 是否与故障有关。需要对齐以下症状与时间信息:

  • 用户看到的是错误、超时、吞吐下降,还是进程退出。
  • 故障首次发生和恢复的时间。
  • 受影响的实例、接口和请求范围。
  • 同期的发布版本、扩容、流量变化和下游异常。
  • CPU、RSS、堆、线程、GC 与容器限制的变化。RSS 表示进程实际驻留的物理内存。

这些证据要来自同一实例和时间窗口。比如,线程池中的空闲线程正常等待,与故障时请求全部堵在下游调用上,含义完全不同。

3. jcmd​

jcmd 为 HotSpot 的多种诊断操作提供统一入口。先查看目标 JVM 支持哪些命令,再记录版本、启动命令和实际参数:

jcmd -l
jcmd <pid> help
jcmd <pid> VM.version
jcmd <pid> VM.command_line
jcmd <pid> VM.flags

命令集合由 JDK 版本与实现决定,所以先执行 help,采集工具也应与目标 JDK 匹配。需要查看系统属性时,可使用 jcmd <pid> VM.system_properties,记录前应去除其中的敏感配置。

jcmd 通过 Attach 机制连接正在运行的 JVM。它要求工具与目标进程位于同一机器,并使用相同的有效用户和组。连接失败时,还要检查容器的 PID 命名空间,以及是否设置了 -XX:+DisableAttachMechanism。该选项会关闭 Attach。

4. 症状与首选证据​

工具应根据当前要回答的问题选择。下表把常见症状与首选证据对应起来,先缩小范围,再按需要补充后续证据:

症状首选证据后续证据
CPU 高OS 线程 CPU 与 CPU profile。GC、JIT 和线程栈。
请求卡住多份线程栈与 wall-clock profile。锁、队列与下游指标。
堆增长GC 后 live set 与类直方图。堆转储中的保留关系。
RSS 高、堆稳定NMT、BufferPool 与线程数。smaps 与 native profiler。
GC 停顿GC 日志与 JFR。分配样本和对象图。
进程崩溃hs_err_pid 与系统退出记录。core dump 与本地符号。
类冲突类加载日志与实际来源。依赖树与加载器统计。

表中的 live set 是 GC 后仍存活的对象总量,类直方图按类统计实例数和占用。NMT 是 HotSpot 的本地内存跟踪功能,BufferPool 提供缓冲池统计。smaps 则显示 Linux 进程的内存映射,便于继续分析堆外增长。

进程崩溃时,hs_err_pid 记录 JVM 致命错误,core dump 保存崩溃现场的进程内存。本地符号用于把机器码地址对应到函数。类冲突时,则应对照实际加载来源与加载器统计。

例如,CPU 高需要先知道哪些线程在计算,堆转储回答的则是对象由谁持有。请求卡住时,还要对照锁、线程池队列和下游指标;分析堆增长时,可用 JFR 对象统计辅助寻找增长类型。

5. 快照与时间序列​

快照记录的是一个时刻,时间序列记录的是一段时间内的变化。一份线程转储只能说明采集瞬间线程在哪里。间隔几秒采集 3—5 份,就能观察线程是否持续停留在计算、锁或 I/O 中,还是一直向前执行。

实际 CPU 占比还需结合线程 CPU 统计或采样结果。同样,一份堆转储显示某一时刻的引用图,单凭它难以证明对象持续增长。先用 GC 日志、JFR 或连续统计确认趋势,再用引用图定位持有者。

6. JFR​

JFR(Java Flight Recorder)是 JVM 内置的事件记录工具。它把运行事件记录到同一时间轴,便于判断延迟与 GC、锁等待或编译是否同时发生。故障期间可以用 profile 配置做短时记录:

jcmd <pid> JFR.start name=incident settings=profile duration=2m filename=/logs/incident.jfr

JFR 可以记录 CPU 样本、线程、分配、锁、GC、类加载、编译和 I/O 事件。profile 开启更多事件,default 适合较低开销的持续记录,实际开销取决于启用事件和阈值。

提前开启持续记录,可以保留故障发生之前的变化。通过最大年龄和大小限制滚动保留范围,再在需要时导出:

jcmd <pid> JFR.start name=continuous settings=default maxage=30m maxsize=256m
jcmd <pid> JFR.dump name=continuous filename=/logs/incident.jfr

第一条命令最多保留 30 分钟、256 MB 的记录。第二条命令导出已有事件,持续记录本身继续运行。文件由目标 JVM 写入,因此路径需要在目标进程中可访问,并具备写入权限和可用空间。

用 JFR 关联暂停的方法见GC 停顿分析。

7. async-profiler​

async-profiler 通过采样统计调用路径的占比。分析 CPU 消耗和请求耗时,需要选择不同的采样目标:

asprof -e cpu -d 30 -f cpu.html <pid>
asprof -e wall -d 30 -f wall.html <pid>

CPU profile 采样处理器正在执行的路径,适合找计算热点。wall-clock profile 按实际经过的时间采样,还包含运行、阻塞和睡眠线程。因此,它更适合分析锁、I/O 等等待造成的延迟。

async-profiler 还支持分配和锁等事件,并能在 HotSpot 上展示 Java、本地与部分内核调用栈。采样得到的是统计分布,而非每次调用的完整追踪。解释结果时,要结合采样频率、持续时间,以及平台权限和符号是否完整。

8. 采集动作的影响​

诊断动作本身也会消耗资源,所以要先明确缺少什么证据,再决定采集方式。可以按以下顺序逐步扩大采集范围:

  1. 读取已有日志、指标和启动参数,确认故障范围。
  2. 用普通线程转储、短时 JFR 或采样 profile 补充调用路径和时间变化。
  3. 需要统计对象数量时,使用类直方图,并评估堆遍历的影响。
  4. 需要完整引用关系或崩溃现场时,再采集 heap dump 或 core dump,并预留落盘空间。

这个顺序用于选择证据,实际影响还取决于堆大小、线程数、磁盘速度和 JVM 状态。执行前可以查看命令影响:

jcmd <pid> help GC.class_histogram
jcmd <pid> help GC.heap_dump

JDK 21 将这两项命令标为 High impact,即高影响操作。执行前要确认可接受的停顿窗口和可用磁盘空间,并约定停止采集或切换实例的处置方式。完整转储会产生大量 I/O,在资源紧张时还会加重进程负担。

8.1 堆转储与显式回收​

堆转储选项会改变是否先回收,以及文件中包含哪些对象。JDK 21 的 GC.heap_dump 默认请求 Full GC,也就是对整个堆发起回收。添加 -all 后会包含不可达对象,并省去这次回收请求:

jcmd <pid> GC.heap_dump /logs/live.hprof
jcmd <pid> GC.heap_dump -all /logs/all.hprof

以上两种方式按分析目标选一种执行。-all 仍需要遍历堆并写出文件,所以不保证无停顿。它还会把已经不可达、等待回收的对象写入文件。

分析时要记录采集选项,区分仍被持有的对象与待回收对象。否则会把尚未来得及回收的占用,误判为持续保留。

9. 证据的环境信息​

不同实例或发布版本的证据很容易被混在一起。为每份诊断文件附上环境与采集信息,才能确认它们是否来自同一现场:

  • 记录采集时间和时区,以及进程 uptime,也就是已运行时长。
  • 记录主机、容器、Pod 和实例标识。
  • 记录完整 JDK 版本、启动命令和实际 flags。
  • 记录发布版本或 commit。
  • 记录采集命令、持续时间和文件校验值。
  • 标明对应监控图的时间区间,以及当时的流量和负载。

这些信息让线程栈、GC 日志和性能样本能够相互印证,也便于在修复后复现相近负载。

10. 常见问题​

10.1 传统工具的适用范围​

目标 JDK 支持时,jstack、jmap 和 jinfo 仍可按需使用。HotSpot 可以优先从 jcmd 查询对应命令及其影响,再确认工具版本与 Attach 权限。

10.2 在线诊断与堆分析​

10.3 诊断动作与故障现场​

11 · 面试题卡片​

11.1 常用 JVM 诊断工具分别能解决什么问题​

出现公司:字节跳动、阿里巴巴

考察重点:根据症状选择快照或时间序列,说明各工具能回答什么,以及采集会增加哪些负担。

相关内容:故障时间线至采集动作的影响。

参考回答

线程转储看调用和等待,堆转储看对象引用,GC 日志与 JFR 看运行事件,profiler 看一段时间内的热点分布。

  • CPU 高: 用 OS 线程统计和 CPU profile 定位计算热点。
  • 请求卡住: 用多份线程栈和 wall-clock profile 分析等待。
  • 堆增长: 观察 GC 后 live set 与类直方图,再用堆转储查保留关系。
  • RSS 增长而堆稳定: 用 NMT 与系统映射查本地内存。
  • GC 问题: 用统一日志和 JFR 对齐暂停与分配事件。

jcmd <pid> help <command> 可查询命令影响。完整堆转储需要遍历和大量写入,应预留停顿窗口与磁盘空间。

11.2 为什么只采一份线程转储经常无法定位问题​

出现公司:阿里巴巴

考察重点:解释瞬时状态与持续停留的差异,并将线程栈与 CPU、请求延迟和队列变化对齐。

相关内容:快照与时间序列。

参考回答

因为单份转储只显示采集瞬间的位置,线程偶然经过某个方法也会出现在栈中。

间隔几秒连续采集 3—5 份,才能观察它是否长期停在计算、锁或 I/O 中,还是持续向前执行。

CPU 或 wall-clock profile 可以进一步给出采样占比。再与线程 CPU、请求延迟和线程池队列对齐,才能判断这种计算或等待是否与故障有关。

11.3 GC.heap_dump 的 -all 选项会消除停顿吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分省去 Full GC 请求与省去堆遍历,并说明不可达对象怎样影响分析。

相关内容:堆转储与显式回收。

参考回答

-all 无法保证消除停顿,它只省去默认的 Full GC 请求。

转储仍需遍历堆并写出文件,这些工作依然会占用资源。

该选项同时导出可达与不可达对象。因此,分析时要区分仍被引用的对象和等待回收的对象,避免把后者当作泄漏。

12. 参考资料​