JVM 突然宕机是后端开发中最令人头疼的问题之一。进程无声无息地消失,日志里可能什么都没有,重启后一切“正常”,直到下一次崩溃再次发生。这篇文章会梳理一套从“第一现场”到“深度尸检”的分析流程,重点讲清楚 hs_err_pid 日志怎么读,以及 gdb、jhsdb、sa-jdi.jar 这三个工具分别在什么场景下用、怎么用。
在动手之前,首先要判断 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 的“临终遗言”,信息量极大,但文件可能长达几万行。不需要全看,盯住几个关键位置就够了。
文件开头几行会写明收到的信号(如 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++ 调用栈、寄存器和汇编指令。
bashgdb /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 模式:
bashjhsdb jstack --exe /path/to/java --core /path/to/core文件
--exe 同样必须是崩溃时的那个 Java 可执行文件。输出就是标准的 Java 线程 dump 格式,包含所有线程的栈、锁信息、线程状态。如果你在排查的是“JVM 在某个操作时崩溃,当时其他线程在干什么”,这个输出比 GDB 的 C++ 栈有用得多。
加上 --mixed 选项可以同时打印 Java 帧和 Native 帧,帧前面带 * 的是 Java 帧,不带的是 C/C++ 帧:
bashjhsdb jstack --mixed --exe /path/to/java --core core文件
jhsdb hsdb如果你需要深入看堆里的对象、类加载器、引用关系,可以启动 HSDB 图形界面:
bashjhsdb 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。
方式一:用 jstack 的 SA 模式
JDK 8 的 jstack 带有一个 -F(force)和 -m(mixed)选项,底层就是调用 SA:
bashjstack -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 图形界面
bashjava -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 文件的话,按问题类型选工具。
jhsdb jstack(JDK 9+)或 jstack -F -m(JDK 8)。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 许可协议。转载请注明出处!