WEBKT

JVM 悄无声息地挂了?没有 hs_err_pid 日志时的排查指南

5 0 0 0

在 Java 运维和开发过程中,最让人头疼的莫过于 JVM 进程突然消失。

通常情况下,如果 JVM 发生致命错误(如 Segfault 段错误、内部 Bug),它的信号处理器(Signal Handler)会尽最大努力在工作目录或 /tmp 目录下生成一个诸如 hs_err_pidxxxx.log 的崩溃日志。这个日志是排查问题的黄金钥匙。

然而,有时候 JVM 就像凭空蒸发了一样,没有留下任何 hs_err_pid 日志。这时,许多开发者会陷入迷茫。实际上,没有生成崩溃日志本身就是一个非常关键的线索——它意味着 JVM 并不是自己由于内部致命错误主动崩溃的,而是被外部力量强行抹杀,或者死于无法做出任何反应的极端情况

本文将深入拆解 JVM 在没有 hs_err_pid 日志时的几种典型“死因”,并给出具体的排查工具和命令。


一、 死因一:Linux OOM Killer 强行超度(最常见)

当系统物理内存不足,且 Swap 空间也耗尽时,Linux 内核为了保护自身不至于崩溃,会触发 OOM Killer(Out of Memory Killer) 机制。它会通过一定的算法算出一个得分最高的进程(通常是占用内存最大、且运行时间长但不那么核心的进程,比如 JVM),然后直接发送 SIGKILL (信号 9) 强杀它。

由于 SIGKILL 信号是不可被捕获、阻塞或忽略的,JVM 根本没有机会执行任何清理代码或信号处理器,自然无法生成 hs_err_pid 日志。

排查方法

运行以下命令,查看系统内核日志中是否有 Java 进程被 OOM Killer 杀掉的记录:

# 方法 1:使用 dmesg 查看,并转换为人类可读的时间戳
dmesg -T | grep -i -E 'oom|kill'

# 方法 2:检查系统消息日志(根据不同系统,路径可能是 /var/log/syslog 或 /var/log/messages)
grep -i -E 'oom-killer|killed process' /var/log/messages
grep -i -E 'oom-killer|killed process' /var/log/syslog

如果看到类似下面的输出,说明你的 JVM 确实死于系统级内存耗尽:

[Xxx xxx xx xx:xx:xx 202X] Out of memory: Kill process 12345 (java) score 850 or sacrifice child
[Xxx xxx xx xx:xx:xx 202X] Killed process 12345 (java) total-vm:15623400kB, anon-rss:8124500kB, file-rss:0kB

解决方案

  • 减少 JVM 的 -Xmx(最大堆内存)设置,给物理机或容器预留更多的堆外系统内存。
  • 检查是否有严重的物理内存泄露(如 DirectByteBuffer、JNI 本地内存泄露等)。
  • 调整 Linux 的 vm.overcommit_memoryvm.panic_on_oom 参数。

二、 死因二:外部进程发送了 SIGKILL (Kill -9)

除了系统内核,外部的用户、脚本或容器管理组件也可能向 JVM 进程发送了 SIGKILL 信号。

在容器化(Kubernetes/Docker)环境中,这种情况尤为常见:

  • K8s Liveness Probe(存活探针)失败:如果容器健康检查连续失败,K8s 会直接杀掉容器重建。
  • 容器内存超出 Limit:如果你的 Pod 设置了内存 Limit(例如 limits.memory: 4Gi),一旦整个容器(包括 JVM 堆、堆外内存、各种 C-Heap 等)的总内存占用超过 4Gi,Cgroup 就会直接给进程发送 SIGKILL

排查方法

  1. 如果在 Kubernetes 中
    通过 describe 命令查看 Pod 的历史事件,重点关注 Last StateReason

    kubectl describe pod <pod-name>
    

    如果看到 OOMKilled: true 或者 Exit Code 是 137(128 + 信号 9 = 137),这就证实了是 Cgroup 内存超限导致的强杀。

  2. 如果在常规 Linux 虚机/物理机中
    查看进程退出状态码。如果你的 JVM 是通过 Bash 脚本启动的,可以在脚本退出时打印 $?。退出码为 137 同样代表收到了 SIGKILL
    此外,可以通过审计日志(Auditd)来追踪是谁发送了信号:

    ausearch -sc kill
    

三、 死因三:JNI/Native 代码直接调用了 exit() 或 abort()

Java 程序可以通过 JNI(Java Native Interface)调用 C/C++ 编写的动态链接库。如果这些 Native 代码内部发生了不可恢复的错误,直接调用了标准 C 库的 exit()_exit()abort() 函数,整个进程会立即终止。

这种终止是操作系统级别的进程主动退出。由于它不属于 JVM 内部检测到的硬件异常(如 SIGSEGVSIGFPE),JVM 根本没有机会触发其内部的 Crash Handler,因此也不会产生 hs_err_pid 日志。

排查方法

  1. 检查退出状态码

    • 如果调用的是 exit(code),进程会以该 code 正常退出(比如退出码 12 等,而非 137139)。
    • 如果调用的是 abort(),会向进程自身发送 SIGABRT(信号 6)信号,退出码通常是 134 (128 + 6)。
  2. 启用核心转储(Core Dump)
    由于没有 hs_err_pid,我们需要操作系统的 Core Dump 文件来还原死前的现场。

    # 临时启用 core dump(大小无限制)
    ulimit -c unlimited
    
    # 查看/设置 core dump 文件的生成路径
    cat /proc/sys/kernel/core_pattern
    

    当进程再次异常退出并生成 core.xxxx 文件后,使用 GDB 进行调试:

    gdb $JAVA_HOME/bin/java core.xxxx
    # 在 gdb 交互界面输入 bt (backtrace) 查看调用栈
    (gdb) bt
    

    这能帮你直接定位到是哪个 C/C++ 库里的哪一行代码执行了 exitabort


四、 死因四:JVM 内存极其匮乏,以至于无法写出日志

这是一种极端而有趣的情况。虽然 JVM 试图去捕获并处理致命错误(比如段错误),但此时系统的虚拟内存、堆栈空间或者磁盘空间已经极端匮乏。

在写出 hs_err_pid 日志时,JVM 需要执行:

  1. 申请一部分临时的 Native 内存。
  2. 在磁盘的指定目录下创建、写入文件。

如果此时系统的文件描述符(FD)已经耗尽,或者磁盘空间 100% 满,或者进程的 C 栈(C-Stack)已经彻底溢出,JVM 的信号处理函数本身也会发生崩溃。这种被称为 Double Fault(双重故障) 的情况会导致 JVM 连最后一封遗书(hs_err_pid)都写不出来就直接挂掉。

排查方法

  1. 检查磁盘和 inode 占用
    df -h    # 检查磁盘空间
    df -i    # 检查 inode 占用(防止小文件过多导致无法创建新文件)
    
  2. 检查文件描述符限制
    ulimit -n
    
    如果一个连接密集的 Java 应用把 FD 占满了,在崩溃时就可能无法打开并写入 hs_err_pid 日志文件。
  3. 改变日志存储路径
    在 JVM 启动参数中,显式指定崩溃日志输出到空间充足、权限正常的路径(如 /tmp):
    -XX:ErrorFile=/tmp/hs_err_pid_%p.log
    

五、 死因五:系统底层硬件故障或内核 Panic

在极少数情况下,底层的物理硬件故障(如内存条 ECC 校验失败、CPU 过热、物理机掉电)或 Linux Kernel 自身的 Bug 会导致整个系统瞬间崩溃、重启或冻结。

在这种系统崩盘的雪崩效应中,JVM 作为一个普通的用户态进程,自然也会瞬间随之消失,没有任何机会留下只言片语。

排查方法

  1. 检查系统的运行时间(Uptime)
    uptime
    last reboot
    
    确认在 JVM 消失的时间点,整台物理机/虚拟机是不是也发生了重启。
  2. 检查硬件错误日志
    在 Linux 下检查 /var/log/mcelog(Machine Check Exception),排查是否有硬件级别的内存纠错失败(Memory ECC error)等报告。

💡 排查路线图总结

当你面对一个悄无声息消失的 JVM 进程时,请按照以下步骤自检:

步骤 排查动作 常见发现 结论与对策
1 检查进程退出码(Exit Code) 137 被强杀(OOM Killer 或 kill -9
134 abort() 信号中断,通常源自 Native 代码
2 检索系统内核日志 (dmesg / messages) Out of memory: Kill process... 确认系统内存耗尽,调整 JVM 堆大小或物理内存
3 检索容器平台事件 (kubectl describe) OOMKilled: true Cgroup 内存超限,调大容器 Limit 或引入 ActiveProcessorCount 等优化参数
4 确认系统资源极限 df -h 满,ulimit -n 过小 系统瓶颈导致 JVM 无法写入 hs_err_pid 日志
5 启用 Core Dump 机制 产生 core 文件,使用 gdb 调试 定位到第三方 JNI 动态链接库(如 .so 文件)内的致命错误

不要因为没有日志而慌张,没有日志本身就是最重要的排查线索。通过向外层(操作系统、容器层)寻找痕迹,任何悄悄消失的 JVM 进程都将无所遁形。

架构黑洞 JVMLinux排查指南

评论点评