编辑
2026-11-06
学习记录
00

目录

JVM 宕机分析实战:从 hs_err 日志到 GDB / jhsdb / SA 的完整指南
先分清:JVM 宕机的两种“死法”
第一现场:hserrpid 日志到底看什么
头部信号与 Problematic frame
崩溃线程的栈
一个容易被忽略的细节:线程数与内存映射
工具一:gdb——看 Native 层的“外科手术刀”
它适合什么、不适合什么
实操命令
一个重要的限制
工具二:jhsdb——JDK 自带的“Java 视角”事后调试器
它解决什么问题
核心用法:jhsdb jstack
GUI 模式:jhsdb hsdb
版本限制
工具三:sa-jdi.jar——JDK 8 时代的 SA 入口
它和 jhsdb 的关系
JDK 8 下的典型用法
必须注意的前提条件
一套可落地的排查流程
最后:比事后分析更重要的事

JVM 宕机分析实战:从 hs_err 日志到 GDB / jhsdb / SA 的完整指南

JVM 突然宕机是后端开发中最令人头疼的问题之一。进程无声无息地消失,日志里可能什么都没有,重启后一切“正常”,直到下一次崩溃再次发生。这篇文章会梳理一套从“第一现场”到“深度尸检”的分析流程,重点讲清楚 hs_err_pid 日志怎么读,以及 gdb、jhsdb、sa-jdi.jar 这三个工具分别在什么场景下用、怎么用。


先分清:JVM 宕机的两种“死法”

在动手之前,首先要判断 JVM 是怎么“死”的,因为这直接决定了后续该看什么文件、用什么工具。

第一种:JVM 自己崩了(Native 层致命错误)。 典型信号是 SIGSEGV(段错误)、SIGBUS、SIGABRT,或者 JVM 内部断言失败、Native 内存分配失败。这种情况下,JVM 会在“临死前”尽量生成一个 hs_err_pid<pid>.log 文件,如果系统开启了 core dump,还会留下一个 core 文件。

第二种:进程被操作系统“杀掉”了。 最常见的是 Linux OOM Killer 因为系统内存不足,直接发 SIGKILL(信号 9)干掉 JVM。这种情况下 JVM 来不及做任何反应,不会生成 hs_err 文件,core 文件里看到的信号也是 SIGKILL。你需要去查 /var/log/messages 或 dmesg 里的 oom-killer 记录。

还有一种容易混淆的情况:Java 堆 OOM(OutOfMemoryError)不是 JVM 宕机。它是 Java 层可捕获的异常,JVM 还活着,不会生成 hs_err 日志。处理方式是 heap dump 分析,和本文讨论的“宕机”是两码事。


第一现场:hs_err_pid 日志到底看什么

hs_err_pid 是 JVM 的“临终遗言”,信息量极大,但文件可能长达几万行。不需要全看,盯住几个关键位置就够了。

头部信号与 Problematic frame

文件开头几行会写明收到的信号(如 SIGSEGV)以及出问题的帧。Problematic frame 那一行最关键:

  • V [libjvm.so+...]:问题出在 JVM 自身,可能是 JIT 编译 bug、GC 实现问题,或者遇到了 JVM 的已知缺陷。
  • C [libxxx.so+...]:问题出在第三方 Native 库(JNI 调用),比如某些加密库、压缩库、网络库。
  • J [com.example.YourClass.method+...]:问题出在 JIT 编译后的 Java 代码执行过程中。

这一行基本决定了你下一步该去找 JVM 厂商/升级 JDK,还是去找那个 Native 库的维护方。

崩溃线程的栈

日志里会包含崩溃线程的完整栈,既有 Java 帧也有 Native 帧。如果是 C 或 V 类型的问题,Native 帧部分能告诉你具体是哪个函数调用出的错。结合业务日志里崩溃前最后几条记录,往往能圈定可疑的代码路径。

一个容易被忽略的细节:线程数与内存映射

如果 hs_err 文件异常巨大(几十 MB 甚至上百 MB),可以看一眼 PROCESS 段里的线程数量和 Dynamic libraries 段。曾经有案例中,日志里出现了三万多个线程,直接指向了线程泄漏问题;同时 Native 内存分配失败(malloc failed)的头部信息,结合线程数爆炸,能快速定位到是创建线程时耗尽的内存。


工具一:gdb——看 Native 层的“外科手术刀”

它适合什么、不适合什么

GDB 是 Linux 下最经典的 C/C++ 调试器。但必须明确一点:GDB 不处理 Java 进程。IBM 官方文档写得很清楚:“gdb does not handle Java processes, so it is of limited use on a pure Java program. It is useful for debugging native libraries and the JVM itself.”

换句话说,你不要指望用 GDB 的 bt 看到 com.example.Service.doSomething 这样的 Java 方法名。它的价值在于:当 hs_err 显示 Problematic frame 是 C [libxxx.so+...] 或 V [libjvm.so+...] 时,用 GDB 加载 core 文件,深入查看崩溃瞬间的 C/C++ 调用栈、寄存器和汇编指令。

实操命令

bash
gdb /path/to/java /path/to/core文件

第一个参数必须是崩溃时那个 JVM 的 java 可执行文件,路径和版本要完全一致。进入 GDB 后:

(gdb) bt

你会看到一串 C/C++ 函数调用。如果栈里有第三方 Native 库的函数,而且恰好是 hs_err 里 Problematic frame 指向的那个库,那基本就锁定了问题来源。如果 GDB 提示找不到某个动态库的符号,可以手动加载:

(gdb) symbol-file /path/to/libxxx.so

然后再次 bt。

一个重要的限制

GDB 分析 core 文件时,看到的是崩溃那一刻的 Native 栈,而不是完整的 Java 线程快照。如果你更关心 Java 层的线程状态、对象分布,不要用 GDB,往下看 jhsdb。


工具二:jhsdb——JDK 自带的“Java 视角”事后调试器

它解决什么问题

jhsdb 是 JDK 9+ 提供的工具,专门用来从 core 文件中恢复 Java 层的视图。它可以像 jstack 一样打印 Java 线程栈,像 jmap 一样查看堆直方图,甚至启动一个 GUI 调试器(HSDB)来浏览对象引用链。

这是 GDB 做不到的:GDB 看到的是 libjvm.so 里的 C++ 函数,而 jhsdb 能直接告诉你“崩溃时 main 线程正阻塞在 java.util.concurrent.LinkedBlockingQueue.take() 上”。

核心用法:jhsdb jstack

最常用、最直接的入口是 jstack 模式:

bash
jhsdb jstack --exe /path/to/java --core /path/to/core文件

--exe 同样必须是崩溃时的那个 Java 可执行文件。输出就是标准的 Java 线程 dump 格式,包含所有线程的栈、锁信息、线程状态。如果你在排查的是“JVM 在某个操作时崩溃,当时其他线程在干什么”,这个输出比 GDB 的 C++ 栈有用得多。

加上 --mixed 选项可以同时打印 Java 帧和 Native 帧,帧前面带 * 的是 Java 帧,不带的是 C/C++ 帧:

bash
jhsdb jstack --mixed --exe /path/to/java --core core文件

GUI 模式:jhsdb hsdb

如果你需要深入看堆里的对象、类加载器、引用关系,可以启动 HSDB 图形界面:

bash
jhsdb hsdb --exe /path/to/java --core core文件

启动后可以浏览线程列表、类浏览器、对象直方图(Object Histogram),甚至用 Inspector 查看某个对象的字段值。注意这需要图形环境(X11 或类似),纯命令行服务器上用不了。

版本限制

jhsdb 是 JDK 9 引入的。Oracle 的文档标注它仍是“experimental and unsupported”,但在实际排查中已经足够稳定。JDK 8 用户需要看下一节的 sa-jdi.jar。


工具三:sa-jdi.jar——JDK 8 时代的 SA 入口

它和 jhsdb 的关系

jhsdb 本质上是对 Serviceability Agent(SA)的封装。在 JDK 8 时代,SA 没有统一的命令行入口,你需要通过 sa-jdi.jar 里的类来启动它。JDK 9+ 的 jhsdb 就是把这些分散的入口整合了起来。所以如果你还在用 JDK 8,sa-jdi.jar 就是你的 jhsdb。

JDK 8 下的典型用法

方式一:用 jstack 的 SA 模式

JDK 8 的 jstack 带有一个 -F(force)和 -m(mixed)选项,底层就是调用 SA:

bash
jstack -J-d64 -F -m /path/to/jdk8/bin/java core文件

-J-d64 表示 64 位模式,-F 强制附加(core 文件场景必须),-m 打印混合栈。如果报错 Can't attach to the process,通常是因为 JDK 缺少调试符号、core 文件被截断,或者权限不足(尝试 sudo)。

方式二:启动 HSDB 图形界面

bash
java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

然后在菜单里选择 File → Attach to Core…,指定 core 文件和对应的 java 可执行文件路径。

必须注意的前提条件

sa-jdi.jar 和 SA 工具链对版本匹配的要求非常苛刻。JDK 版本(包括更新号)必须与生成 core 文件时完全一致,并且该 JDK 最好包含完整的调试符号(debuginfo)。此外,JDK 8u72 之后 SA 默认被禁用,JDK 9+ 逐渐弱化,OpenJDK 17+ 已经彻底移除了 SA 相关代码。如果你的生产环境跑的是高版本 JDK,这条路基本走不通,应该优先用 jhsdb(如果版本支持)或者回到 hs_err + GDB 的组合。


一套可落地的排查流程

把上面所有信息串起来,遇到 JVM 宕机时按这个顺序走:

第 1 步:确认是不是真宕机。 先看 hs_err_pid*.log 有没有生成。没有的话,立刻去 /var/log/messages 或 dmesg 搜 oom-killer,确认是不是被系统杀的。如果是 OOM Killer,问题就从“JVM 为什么崩”变成了“为什么系统内存不够”,方向完全不同。

第 2 步:读 hs_err 的 Problematic frame。 V 开头找 JVM 问题(查 JDK bug 库、尝试升级或降级小版本);C 开头找 Native 库问题(确认库版本、是否有已知缺陷);J 开头是 JIT 编译后的 Java 代码,相对少见,通常需要结合业务代码和 JIT 日志分析。

第 3 步:有 core 文件的话,按问题类型选工具。

  • 关心Java 线程当时在干什么、有没有死锁或阻塞 → jhsdb jstack(JDK 9+)或 jstack -F -m(JDK 8)。
  • 关心Native 层的崩溃现场(C/C++ 调用栈、寄存器、汇编)→ gdb /path/to/java core,然后 bt。
  • 需要深度浏览堆对象、类加载器 → jhsdb hsdb(JDK 9+)或 sa-jdi.jar 的 HSDB(JDK 8)。

第 4 步:交叉验证。 把 hs_err 里的 Native 帧、jhsdb jstack 里的 Java 栈、以及业务日志里崩溃前最后几条记录放在一起看。一个典型的有效线索组合是:hs_err 显示 C [libcrypto.so+...],jhsdb jstack 显示某线程正在执行一个 SSL 相关的方法,业务日志显示崩溃前刚有一次 TLS 握手。三条线交汇,问题基本就锁定了。


最后:比事后分析更重要的事

事后分析再精彩,也不如提前留下证据。几个值得加到启动参数里的配置:

bash
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps -XX:ErrorFile=/path/to/hs_err_%p.log

ErrorFile 确保 hs_err 日志落在你知道的地方,而不是跟着工作目录乱跑。HeapDumpOnOutOfMemoryError 针对的是 Java 堆 OOM(不是宕机),但同样是排查内存问题的标配。至于 core dump,是否开启取决于你对磁盘空间和排查深度的权衡——如果不开 core,gdb 和 jhsdb 就无用武之地,只能靠 hs_err 和日志破案。

本文作者:Weee

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!