WEBKT

如何通过 kmsg 与 Core Dump 100% 判定 Java 进程是被 OOM Killer 杀死还是自愿退出

1 0 0 0

在 Linux 环境中,Java 进程突然消失是一个经典的线上故障。通常,开发者会陷入争论:到底是 JVM 因为内部 OOM(Java heap space)主动退出了,还是触发了操作系统的 OOM Killer 被无情抹杀了?

要做到 100% 闭环确认,不能仅凭猜测。本文将从 Linux 内核信号机制、kmsg 异步日志、Exit Code 以及 Core Dump 的生成机理,提供一套无懈可击的排查判定逻辑。


核心定理:信号与 Core Dump 的物理限制

在开始排查前,必须明确一个关于 Linux 信号(Signal)的硬性物理限制:

  1. OOM Killer 的执行手段是 SIGKILL (Signal 9)
  2. SIGKILL 无法被捕获、阻塞或忽略
  3. 根据 Linux 规范(man 7 signal),SIGKILL 的默认动作是立即终止进程(Term),绝对不会产生 Core Dump

相反,如果 JVM 是因为内部严重错误(如 Native OOM、Metaspace 分配失败导致无法创建线程、或者 JVM 自身 Bug)崩溃,它通常会触发 SIGSEGV (Signal 11) 或调用 abort() 触发 SIGABRT (Signal 6)。这两种信号的默认动作是产生 Core Dump(Core),并且 JVM 在临死前会努力写出一份 hs_err_pid.log 崩溃日志。

基于此,我们可以得出第一层判定逻辑:

现象 \ 原因 操作系统 OOM Killer (SIGKILL) JVM 自愿/非自愿崩溃 (SIGSEGV/SIGABRT/System.exit)
Exit Code 137 (128 + 9) 134, 139, 10
hs_err_pid.log 绝对没有(进程瞬间被杀,无暇写入) (保存在工作目录或 /tmp
Core Dump 文件 绝对没有 可能有(取决于 ulimit -c 配置)
内核日志 (kmsg) 有明确的 Killed process 记录 无 OOM Killer 记录(可能仅有段错误记录)

第一步:检索 kmsg/dmesg 定位内核终结证据

这是最直接、具有法律效力的“第一现场”证据。当 Linux 触发 OOM Killer 时,内核会在环形缓冲区(Ring Buffer)中记录详细的杀进程上下文,这些信息会输出到 /dev/kmsg,并最终被 rsyslogsystemd-journald 收集。

1. 检索命令

不要只用 dmesg,因为在高并发系统上,dmesg 的 Ring Buffer 极易被刷掉。应按以下顺序检索:

# 方式 A:直接检索系统全局日志(适用于 Centos/RHEL/Ubuntu)
grep -iE 'oom[-_]killer|killed process' /var/log/messages
grep -iE 'oom[-_]killer|killed process' /var/log/syslog

# 方式 B:使用 journalctl(适用于 systemd 系统)
journalctl -k --grep="Killed process"
journalctl -g "Out of memory"

# 方式 C:实时读取/历史检索 dmesg
dmesg -T | grep -iE 'oom[-_]killer|killed_process'

2. 完美的 OOM Killer 日志特征分析

如果你在日志中看到如下输出,即可 100% 确认 是系统 OOM:

[72394.102394] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
...
[72394.102450] CPU: 3 PID: 12847 Comm: java Not tainted 5.4.0-150-generic #167-Ubuntu
...
[72394.102510] Mem-Info:
...
[72394.102800] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice,task=java,pid=12847,uid=1000
[72394.102850] Out of memory: Killed process 12847 (java) total-vm:14328472kB, anon-rss:7849204kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:28400kB oom_score_adj:0

关键指标解读:

  • Killed process 12847 (java):内核明确记录了 PID 为 12847java 进程被 Kill。
  • anon-rss:7849204kB:被杀瞬间,Java 进程占用的物理内存(RSS)约为 7.48 GB。对比你的 -Xmx 参数,如果 -Xmx 设为 4G,而 RSS 达到了 7.48G,说明是非堆内存(Direct Memory、Metaspace、Thread Stack 或 JVM 自身内存泄漏)侵占了物理内存。

3. 容器环境(CGroup OOM)的特有日志

如果 Java 运行在 Kubernetes 或 Docker 容器中,触发的是容器 CGroup 限制,日志特征会有所不同:

[10294.593021] memory: usage 2097152kB, limit 2097152kB, failcnt 48201
[10294.593025] memory+swap: usage 2097152kB, limit 2097152kB, failcnt 0
...
[10294.593110] Memory cgroup out of memory: Killed process 12847 (java) total-vm:4194304kB, anon-rss:2095104kB, file-rss:2048kB, shmem-rss:0kB

看到 Memory cgroup out of memory,说明该 Java 进程超出了容器设定的 Memory Limit(如 Kubernetes 中的 resources.limits.memory),同样属于被强杀。


第二步:排除法分析 Core Dump 与 hs_err 日志

如果在 kmsg 中没有找到任何 OOM 的痕迹,但进程依然挂了,我们需要通过 Core DumpJVM 崩溃日志 进行反向排除。

1. 检查是否存在 Core Dump

在配置了 ulimit -c unlimited 且设置了 /proc/sys/kernel/core_pattern 的系统上,JVM 异常崩溃会留下 Core 文件(如 core.12847 或通过 coredumpctl 管理)。

  • 尝试定位 Core 文件
    # 使用 systemd-coredump 的系统
    coredumpctl list | grep java
    
  • 如果找到了 Core Dump
    使用 GDB 加载 Core 文件,查看导致进程终止的信号。
    gdb $JAVA_HOME/bin/java core.12847
    
    进入 GDB 后,第一行通常会显示:
    Program terminated with signal SIGSEGV, Segmentation fault.
    # 或者
    Program terminated with signal SIGABRT, Aborted.
    
    100% 判定结论:只要 GDB 显示是以 SIGSEGVSIGABRT 结束,并且产生了 Core Dump,就绝对不是系统的 OOM Killer 所为。这属于 JVM 内部 native 代码越界、内存分配失败主动放弃、或者被外部显式发送了 kill -6 / kill -11 信号。

2. 检查 hs_err_pid.log

如果 JVM 是因为自身检测到致命错误退出的,它会在工作目录下生成一个名为 hs_err_pid<pid>.log 的文件。打开此文件,查看首行:

  • 如果是 Native OOM

    # There is insufficient memory for the Java Runtime Environment to continue.
    # Native memory allocation (malloc) failed to allocate 32768 bytes for ChunkPool::allocate
    

    这证明是 JVM 申请系统物理内存失败,由 JVM 主动触发 abort() 退出。虽然是内存问题,但它不是被 OS OOM Killer 杀死的,而是 JVM 自己发现无路可走自杀的。

  • 如果是空
    若既没有 hs_err_pid.log,也没有 Core Dump,且系统 ulimit -c 已经打开,那么大概率是遇到了 SIGKILL(OOM Killer)。


第三步:核对退出状态码(Exit Code)

如果 Java 进程是由 Shell 脚本启动,或者在 Kubernetes/systemd 下运行,守护进程通常会捕获到 Java 进程退出的 Exit Code。

在 Bash 中,当进程因为收到信号而退出时,其 Exit Code 的计算公式为:
$$\text{Exit Code} = 128 + \text{Signal Number}$$

  • Exit Code 137:$128 + 9$ (SIGKILL)。
    • 结论:进程收到了 SIGKILL。可能原因:
      1. 系统 OOM Killer 强制终结。
      2. 人为执行了 kill -9 <pid>
      3. 容器编排工具(如 Kubernetes)因为 Readiness/Liveness Probe 失败,超时后强制执行 SIGKILL
  • Exit Code 134:$128 + 6$ (SIGABRT)。
    • 结论:进程自己调用了 abort()。例如 JVM 触发了致命错误、-XX:+CrashOnOutOfMemoryError 触发、或者执行了 kill -6。这属于自愿或可控退出
  • Exit Code 139:$128 + 11$ (SIGSEGV)。
    • 结论:段错误。JVM 访问了非法内存地址,通常是 C++ native 库的 bug。

终极判定流(Decision Tree)

将以上步骤整合,我们可以得到如下 100% 确认的诊断决策流:

                    [ Java 进程突然消失 ]
                              │
                    检查系统日志 (kmsg/dmesg)
                              │
             ┌────────────────┴────────────────┐
      找到 OOM-Killer 记录              无 OOM-Killer 记录
             │                                 │
     [100% 判定:OS OOM 强杀]             检查进程退出码 (Exit Code)
                                               │
                                ┌──────────────┴──────────────┐
                            Exit Code = 137             Exit Code != 137
                                │                             │
                        检查是否有人为                检查是否有 hs_err.log
                       "kill -9" 或容器超时            或 Core Dump (SIGSEGV/SIGABRT)
                                │                             │
                        ┌───────┴───────┐             ┌───────┴───────┐
                       是              否            有              无
                        │               │             │               │
                 [人为/容器干预]  [疑似隐蔽OOM]  [JVM 内部崩溃/  [主动 System.exit
                                  (需排查Cgroup)   Native OOM]   或正常退出]

黄金排查命令行

为了提高日常排查效率,可以直接使用以下一句话脚本,一键扫描当前系统的 OOM Killer 历史痕迹:

printf "\n=== 1. 检查内核 OOM 记录 ===\n" && \
(dmesg -T 2>/dev/null | grep -iE 'oom[-_]killer|killed process' | tail -n 5 || echo "dmesg 无记录") && \
(grep -iE 'oom[-_]killer|killed process' /var/log/messages 2>/dev/null | tail -n 5 || echo "/var/log/messages 无记录") && \
printf "\n=== 2. 检查 CGroup (容器) OOM ===\n" && \
(journalctl -k --grep="killed process" --no-pager 2>/dev/null | tail -n 5 || echo "journalctl 无记录") && \
printf "\n=== 3. 寻找最近的 JVM 崩溃日志 ===\n" && \
find . -name "hs_err_pid*.log" -mtime -1 2>/dev/null

通过上述方法,你可以明确区分出“因物理内存不足被系统抹杀(OOM Killer)”与“因内部错误或主动逻辑退出(JVM Voluntary Exit)”的界限,为后续的架构优化(如调整 -Xmx 与 容器 Limit 比例,或排查堆外物理内存泄漏)提供铁证支撑。

SRE神盾局 LinuxJVMOOM Killer

评论点评