跳过正文
死后 JVM 崩溃分析:用 jcmd 读 core,而不是重学一套工具
  1. 文章/

死后 JVM 崩溃分析:用 jcmd 读 core,而不是重学一套工具

·5040 字·11 分钟
NeatGuyCoding
作者
NeatGuyCoding

死后 JVM 崩溃分析:用 jcmd 读 core,而不是重学一套工具
#

HotSpot 进程一旦以 SIGSEGV 等方式退出,运维手里通常有两类工件:JVM 写的 hs_err_pid*.log,以及操作系统写的 core dump(Windows 上常见为 minidump)。前者像「案发现场摘要」,后者才是完整内存镜像。多年来,要在 core 里看到 Java 线程、堆、类元数据,往往得走 Serviceability Agent(SA)jhsdb 这条独立链路;而线上排障早已习惯 jcmd 的子命令。 JEP 528: Post-Mortem Crash Analysis with jcmd 的目标,是把 同一套诊断命令 延伸到 Linux core / Windows 死后环境,通过 process revival 映射崩溃时的 VM 状态,而不是再维护一套 Java 层的镜像实现。

截至 JDK 24/25Oracle jcmd 手册,Synopsis 仍只描述对 存活 PID 的附着;jcmd core.1234 …-L-c 等语法以 JEP 正文为准,落地前应以你实际使用的 JDK 构建验证。JEP 状态为 Candidate,目标 Release 27——下文能力边界以 JEP 与可复现命令为主,演讲中的分支名、平台细目等单独标注。

在线排障里,JMX(JConsole、JMC)、JFRJDI/JVMTI 与统一 JVM 日志各自覆盖监控、录制与断点场景;它们依赖 进程仍存活或可连。一旦 JVM 已死,上述通道大多失效,剩下 hs_err + core + 原生调试器 + SA/jhsdb。JEP 528 要补的,是其中 Java 级视图 这一段与 jcmd 对齐,而不是再发明第四种 CLI 方言。

JavaOne 标题页:Post-Mortem Crash Analysis with jcmd,并链到 openjdk.org/jeps/528

Serviceability Tools:jcmd 本地 attach,示例命令 Thread.print、GC.heap_dump

图:JDK 可服务性工具谱系中,jcmd 承担本地 attach 与 Thread.print / GC.heap_dump 等子命令——死后扩展意在延续这一入口。

图:会议标题与 JEP 528 入口——死后诊断被定位为 jcmd 的扩展,而非全新 CLI。


崩溃工件:文本摘要与内存镜像各管什么
#

为什么:生产事故里最先需要的是「能转发、能全文检索」的线索;但 hs_err 给不出可遍历的堆图,也无法替代「某对象在崩溃瞬间是否仍被引用」这类问题。

机制/约束JEP 528 Motivation 将分工写得很清楚:HotSpot 在致命错误时写出 hs_err_pidXXX.log(失败线程栈、已加载库、VM 版本等);OS 将进程地址空间保存为 core dump。Windows 上演讲常用 minidump 一词;JEP 正文仍以 core dump 统称,未规定 Windows 文件格式细节(属 OS/运维范畴)。

怎么做

# 收集(Linux 示例)
cp hs_err_pid*.log /case/run-20260517/
cp core.* /case/run-20260517/
ls -la /case/run-20260517/

# 目标侧:允许生成 core(依发行版固化到 runbook)
ulimit -c unlimited

常见误区:只留 hs_err 就关闭工单——许多 Metaspace / 堆 / 锁 问题需要 core 上的 GC.* / VM.*;反过来,只拷 core 却不留 hs_err,会丢掉 JVM 自报的 错误类型、编译线程、VM 参数 等上下文。

幻灯片 Crash dumps:hs_err_pidXXX.log 由 JVM 写出,Core/Minidump 由 OS 保存进程内存镜像

图:hs_err 为文本摘要;core/minidump 为 OS 级内存镜像——死后分析通常两者都要。


原生调试器够不到 Java 语义
#

为什么gdb / WinDbg 擅长 库、符号、原生栈;崩溃若发生在 JIT 代码、解释器、GC 屏障 附近,工程师更需要 哪条 Java 线程、锁谁持有、堆顶是什么

机制/约束:JEP 明确:用 gdb 分析 core 时,无法把 JVM 内部结构直接解释成应用级 Java 状态;解码某个 oop 的 class 需要手工读对象头(参见 JEP 450 对象头)。JEP Alternatives 同时强调:原生调试器仍是 JDK 排障的必要组成部分——jcmd 不是要取代 gdb 的任意地址检视。

怎么做

gdb /path/to/java core.1852094
# 同一 core,JEP 目标语法(GA 可能尚未支持):
/path/to/jdk/bin/jcmd core.1852094 Thread.print

常见误区:在 ?? () 帧上纠结「缺符号」却忽略 Java 栈;或期望 WinDbg 单独给出 java.lang.Thread.State——那是 JVM 诊断层的事。

Native debuggers:gdb 可解析 oopFactory::new_objArray,但大量帧为 ?? ()

图:原生栈里 JVM C++ 符号可见,JIT/解释路径常解析为 ?? (),无法直接对应 Java 方法。


从 SA 双轨维护到单一 jcmd 入口
#

为什么HotSpot Serviceability 中的 SA 能在 运行进程与 core 上暴露 Java 堆与 HotSpot 结构,但 JEP Motivation 指出其实现 brittle、dated,需随 VM 演进持续维护。在线场景里,jcmdAttach API 已承载 Thread.printGC.heap_dump 等;死后若再维护 jhsdb + SA 平行实现,成本高且易漂移。

机制/约束部分为演讲者观点):演讲提到 SA 与 HotSpot 源码 约 13 万行 级镜像同步、ZGC 支持不足等——JEP 未给出行数或 ZGC 细节。JEP Goals 是聚焦 jcmd降低jhsdb/SA 的维护投入,并非 立即移除现有工具。

怎么做(统一入口示意):

# 存活 JVM(GA 已支持)
jcmd <pid> Thread.print

# 死后(JEP 528 目标)
jcmd core.1852094 Thread.print

常见误区:在 JEP 落地前假设 jstack/jmap 已废弃——它们仍广泛存在于旧 runbook;死后分析应规划迁移到 jcmd + core,而非混用多套脚本且版本不一致。


Core revival:映射内存,复用既有 DCmd
#

为什么:若死后诊断重写一套「Java 镜像」,将与 HotSpot 内部结构强耦合;JEP 选择 子进程 revival:把 core 按原虚拟地址 映射回来,使 指针仍有效,再调用与 live Attach 相同的 native 诊断实现

机制/约束Reviving a core dump 规定:

  1. jcmd 启动子进程,mmap core,恢复 Java heap、元空间、线程栈 等;
  2. 在原地址加载崩溃时的 libjvm.so(Windows 为 jvm.dll,演讲用语);
  3. 不执行 Java 代码、不触发 GC;可调用解释数据结构的 native JVM 函数
  4. 事后分析只需 core + 崩溃 JVM 二进制不必 加载业务 .so 全集(跨机搬运友好)。

OpenJDK jmm.h 中的 ExecuteDiagnosticCommand 与 live 路径一致;幻灯中的 DCmd::parse_and_execute 为实现层说法,非 JEP 正文符号

Mermaid diagram 1

怎么做(sandbox 试验构建,分支名未在公开 openjdk/jdk 验证,属演讲者口径):

# 需 JEP 528 实现所在的 JDK 构建
build/*/images/jdk/bin/jcmd -L /prod/jdk/lib/server/libjvm.so ./core.1852094 Thread.print

常见误区:以为 revival 会 继续跑业务线程——子进程只是 只读重演内存布局;在 core 上执行 GC.runJFR.start运行态 命令不符合 JEP Non-Goals / Future Work(Java 实现的 DCmd 与 revival 不兼容)。

Revival Details:jcmd 经 Attach API 向 Live Process 发送命令,底层 DCmd::parse_and_execute

图:live 路径上 jcmd → Attach API → DCmd::parse_and_execute;死后改为对 revival 子进程中的 VM 映像发同类命令。

Memory map sketch:Original Process 与 Revived helper 在相同 VA 映射 heap、栈与 libjvm.so

图:helper 从 core 映射 Java Heap、线程栈 等于崩溃 VA;helper 自身 libc 可与生产不同地址,不影响 JVM 结构解读。


Thread.print:锁与 safepoint 的可信度
#

为什么:死后最先要回答的往往是 谁阻塞谁、崩溃线程卡在何处

机制/约束:JEP 将 Thread.print 列入死后 26 个可用命令之一(相对 live 约 57 个)。输出含 java.lang.Thread.StateBLOCKED (on object monitor)_at_safepoint 等,与 live 线程转储 同类

怎么做

jcmd core.1852094 Thread.print > threads-at-crash.txt
# 与事故前 live 抓取对比(若有)
diff threads-live.txt threads-at-crash.txt

常见误区:把死后输出里的 cpu=0.95ms 当作性能数据——演讲者观点:对已死进程 CPU 时间无意义;JEP 示例行仍含 cpu= 字段,规范未定义 core 上该字段语义,分析时应忽略。

Thread.print 对比:Thread-2 BLOCKED,waiting for monitor entry,_at_safepoint

图:jcmd core.1852094 Thread.printThread-2BLOCKED (on object monitor),HotSpot 侧 _at_safepoint,栈顶 Threads$ThreadRunnable.runnableMethod2


堆诊断:GC.heap_infoGC.heap_dumpGC.class_histogram
#

堆布局与 hprof 导出
#

为什么hs_err 里堆地址需要人工拼接的时代,导出 .hprof 往往依赖 SA 脚本;统一 jcmd 可降低 runbook 碎片。

机制/约束:JEP 死后列表含 GC.heap_infoGC.heap_dump。GA 手册对 live 的语义: GC.heap_info 报告布局;GC.heap_dump 生成 .hproffilename-all 等选项)。core 上是否支持这些选项的组合,以实现为准

怎么做

jcmd ./core.1852094 GC.heap_info
jcmd ./core.1852094 GC.heap_dump /tmp/postmortem.hprof

常见误区:认为 GC.heap_dump 会触发 GC——在死后 revival 中 不发生 GC;导出的是 崩溃瞬间冻结的堆图

类直方图
#

为什么:需要快速判断 byte[]、某业务类 是否在崩溃时异常膨胀,而不立刻开 MAT。

机制/约束GC.class_histogram 在 GA 中标记 Impact: High(与堆大小相关);死后输出为 crash site 快照#instances / #bytes 列与 live 格式一致。模块后缀如 (java.base@27-internal) 来自演示 JDK,非规范固定字符串

怎么做

jcmd core.1852094 GC.class_histogram | head -40

常见误区:用直方图做 引用链分析——它只有 按类聚合;引用链仍需 heap dump 或 JEP Future Work 中的 任意对象检视(演讲称规划中的 VM.inspectJEP 未使用该符号名)。

![GC.class_histogram:8802 个 B 实例约占 392760 字节,java.lang.String 等紧随其后

图:GC.class_histogram 前几行:[B 8802 实例、392760 字节;java.lang.Classjava.lang.String 等带 java.base@27-internal 模块标注。


类与元空间:VM.classesVM.classloader_statsVM.metaspace
#

已加载类与初始化状态
#

为什么:崩溃若发生在 类初始化、<clinit>、复杂 ClassLoader 树 上,需要知道类是否 fully_initialized

机制/约束:JEP 死后命令含 VM.classesVM.classloader_stats(注意官方全名,不是 演讲口语 ClassLoader.stats)。 VM.classes 支持 -verbose 输出更细字段;幻灯 OCR 中的 KlassAddrStateFlags 手册未逐列保证,以目标构建输出为准。

怎么做

jcmd core.1852094 VM.classes | rg 'MyApp|fully_initialized'
jcmd core.1852094 VM.classloader_stats

常见误区:脚本里写 ClassLoader.stats 导致 命令不存在——应改为 VM.classloader_stats

VM.classes:KlassAddr、fully_initialized、java.lang.Shutdown 等类元数据列

图:VM.classes 表头含 KlassAddr Size State Flags ClassName;示例行 fully_initializedjava.lang.Shutdownjdk.internal.misc.Blocker 等。

Metaspace 用量
#

为什么Metaspace OOMEclass space 压力类崩溃,需要在死后核对 used / committed / reservedshared classes

机制/约束:JEP 含 VM.metaspace;手册列有 basicshow-loadersby-chunktype 等选项。默认输出粒度 以实现为准

怎么做

jcmd core.1852094 VM.metaspace > meta-at-crash.txt

常见误区:只盯 used 忽视 reserved——与 hs_err 对照时,两者可能讲述 提交 vs 预留 的不同侧面;应以 VM.metaspace 易扫读行为主、hs_err 为交叉验证。

VM.metaspace:Metaspace used 72K,497 classes,492 shared

图:Metaspace used 72K, committed 192K, reserved 1114112K497 classes (492 shared) 及 chunk 级 Non-Class / Class 分段。


CLI:-L-chelp 的匹配规则
#

为什么:core 常被拷到笔记本分析;jcmd 版本崩溃 JVM 版本 可能不同,但 libjvm.so 必须对齐

机制/约束(JEP post-mortem environments):

  • -L /path/to/libjvm.so:指定崩溃 JVM 的二进制;
  • -c ./core:参数为 core 文件,避免与 main class 名 冲突;
  • 分析用 jcmd 可与崩溃 JDK 不同版本,但须能加载 崩溃时的 JVM 二进制
  • 死后环境须与崩溃点 相同 OS 与 CPU 架构

演讲者观点jcmd <core> help 只列出 core 可用子集(JEP 用「57 中有 26 个」描述范围,未单独定义 help 过滤算法);OL9/RHEL9 上与生产一致的 libc.so 映射可降低 revival 失败率——非 JEP 条文

怎么做

jcmd -c ./core.1852094 help
jcmd -L /opt/jdk-prod/lib/server/libjvm.so ./core.1852094 Thread.print

常见误区:笔记本装了 JDK 21 的 jcmd,却用 JDK 17 的 core 却不传 -L 指向 17 的 libjvm.so;或把 core 拷到 不同架构 的机器上分析。

jcmd on cores:Interface 示例 jcmd $COREFILE help

图:死后接口从 jcmd <pid> 扩展为对 $COREFILE 执行 help 与子命令。


能力边界与交付节奏
#

为什么:避免对 未 GA 能力过度承诺,也避免用 jcmd 替代 所有 原生调试。

机制/约束

主题JEP / 事实演讲者补充
gdb 关系Non-Goals:不能替代原生调试器WinDbg 场景类比
运行态命令死后 JFR.start/JFR.stopGC.run 亦不在死后列表
任意对象检视Future Work:inspect arbitrary Java objects口语 VM.inspect无 JDK 27 排期承诺
平台Goals:Linux + Windows;Future Work:macOS细化为 x64 / AArch64 三元组
状态CandidateRelease 27JDK-8328351sandbox 分支 jcmd_core_process_revival 未在公开 GitHub 检出

怎么做(边界自检):

jcmd core.1852094 help | rg -i jfr    # 预期无 JFR.start/stop

常见误区:在 core 上找 JFR.start;或认为 jcmd 能 inspect 任意地址的对象——当前应使用 gdb/jhsdb,直至 Future Work 落地。

Current Work:JEP 528 覆盖 Linux x64、aarch64、Windows x64 上 broad selection of jcmd commands

图:JEP 528 将 broad selection of jcmd commands 带到 Linux x64 / Linux aarch64 / Windows x64;工程状态为向 code review 推进(幻灯口径)。


建议的死后排查顺序
#

若你维护的是 HotSpot 崩溃 runbook,在 JEP 528 可用后,可按「先廉价、后沉重」收敛证据链:

Mermaid diagram 2

  1. hs_err:信号、问题线程、VM 版本、-XX: 参数。
  2. Thread.print:锁、Java 栈、safepoint。
  3. VM.metaspace / VM.classes / VM.classloader_stats:元空间与类加载器。
  4. GC.class_histogram:对象组成是否异常。
  5. 需要 MAT 时 GC.heap_dump
  6. VM 原生缺陷或 jcmd 无法覆盖的地址,再用 gdb

参考与延伸阅读
#

相关文章

Spring Boot 4 技术栈纵览:Starter 粒度、MVC 版本协商与安全演进

·4493 字·9 分钟
Spring Boot 4 与 Spring Framework 7 组合下,依赖可按能力拆成更细的 Starter,Web 出站客户端可与服务端 MVC 依赖分离;同一代码库可在 Spring MVC 中启用内置 API 版本解析,并与 Spring Data JDBC、RestClient / 声明式 @HttpExchange 客户端协同。Spring Security 7 侧重可叠加的 Customizer<HttpSecurity>、一次性令牌登录、WebAuthn 与注解式多因子模型;