日志没异常,Java 进程却没了?OOM Killer 的判决书在 dmesg 里
场景:Java 进程半夜突然消失,应用日志里连一个异常都没有——它被内核判了死刑 路径:进程消失现场 → dmesg 翻出 OOM 记录 → oom_score 看懂"为什么是它" → 内核兜底原理 → 预防处方
症状识别
Java 进程消失了,日志里却什么都没有
上篇讲了 free 的 MemAvailable 才是判断内存够不够的真值,这篇我们把内存问题推到最极端——进程直接没了。
Java 进程半夜消失,应用日志却干干净净——这不是代码 bug,是内核给你判了"死刑"。
某天凌晨,监控告警:支付服务挂了。不是 RT 飙高、不是错误率上升,是进程从机器上消失了。登录上去 ps -ef | grep payment,一行输出都没有(同机的另一个 Java 进程 report-service 还活着)。值班同学的第一反应是"重启"——重启前习惯性翻了翻应用日志,结果后背发凉:
从崩溃前一刻到最后一条日志,中间一个异常都没有。 没有 OutOfMemoryError,没有 StackOverflowError,没有信号退出码,日志的最后一行还停在一条正常的业务请求上。

如果你遇到过这种"静默消失",第一反应往往有两个:
- "是不是代码出 bug 了?" —— 但 JVM 崩溃一定会留堆栈,日志干净说明不是应用层的事。
- "是不是被人 kill 了?" —— 查
last看登录记录,没有人登录,没有kill -9的手动痕迹。
两个都排除后,真相指向一个经常被忽略的系统层机制——OOM Killer。它不是应用层代码写错,而是内核在内存耗尽时执行的一次"主动击杀"。而它留下的所有线索,都写在一份你可能从没打开过的日志里。
OutOfMemoryError 和 OOM Killer 是两码事
先厘清一个高频混淆点:Java 的 OutOfMemoryError(OOM) 和 Linux 的 OOM Killer 完全不是一回事。
| Java OutOfMemoryError | Linux OOM Killer | |
|---|---|---|
| 谁出手 | JVM 自己 | 内核 |
| 什么情况 | 堆内存不够,JVM 拒绝继续分配 | 整机物理内存耗尽,内核挑进程杀掉 |
| 表现 | 日志有 java.lang.OutOfMemoryError 堆栈 |
进程被 SIGKILL,日志无异常 |
| 留痕 | 应用日志 + 堆 dump | dmesg 内核日志 |
上篇我们说"MemAvailable 持续 < 10% 且 swap 可用空间持续下降(使用率上升)才可能触发 OOM killer"——这篇就是那个场景的真身:进程不是被 JVM 杀死的,是被内核挑出来献祭的。
隐性问题:先怀疑"被杀",再怀疑"崩溃"
遇到进程静默消失,最忌讳直接重启。Naive 的应对是"重启看会不会再崩";有经验的工程师会先问"这台机器在崩溃时刻发生了什么"。而正确做法是:先查系统日志确认是不是 OOM Killer 下的手——因为如果真是它,重启十次也解决不了,明晚同一个进程还会再被杀一次。
诊察
dmesg:OOM Killer 留下的唯一现场
dmesg 是查看内核环形缓冲区日志的命令,OOM Killer 的每一次击杀都会在这里留英文记录。时间戳对不上进程消失时刻?加上 -T 转成人类可读时间:
$ dmesg -T | grep -iE "out of memory|oom_reaper"
[Sat Jul 26 02:13:41 2026] Out of memory: Killed process 12345 (java) total-vm:8388608kB, anon-rss:5832952kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:10240kB oom_score_adj:0
[Sat Jul 26 02:13:41 2026] oom_reaper: reaped process 12345 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

逐字段拆解这行"判决书":
Killed process 12345 (java)—— 内核杀掉的进程 PID 和名字,命中我们要找的那个 Java 进程。total-vm:8388608kB—— 该进程的虚拟内存总量,8GB。anon-rss:5832952kB—— 匿名物理内存(进程堆、栈等非文件映射占用的内存),5.8GB,这是它的"真实体重"。oom_score_adj:0—— 进程的评分调整值,默认 0,后面定位段细讲。oom_reaper: reaped—— 内核的收割线程(oom_reaper,负责强制回收被选进程内存的内核线程)已经把它回收干净了,此时anon-rss:0kB,物理内存被释放。
这行日志就是确认 OOM 的第一现场。 看到它,基本可以下结论:进程不是崩溃,是被内核杀掉的。
free:被杀那一刻内存到底剩多少
确认了凶手,还要确认"案发现场"——被杀那一刻整机内存还剩多少。free -h 看两列:
$ free -h
total used free shared buff/cache available
Mem: 30Gi 29Gi 145Mi 224Mi 988Mi 60Mi
Swap: 8.0Gi 7.4Gi 0.6Gi

free只剩 145Mi —— 完全空闲的物理页几乎没了。available只剩 60Mi —— MemAvailable(上篇讲过的"还能安全分给新程序的内存")逼近 0,这是 OOM 的明确前兆。Swap已用 7.4Gi、只剩 0.6Gi —— 内存不够,内核把匿名页大量换到磁盘,swap 也快打满了,回收空间见底。
两条命令互相印证:dmesg 说"02:13:41 杀了 java",free 显示同一时刻整机可用内存归零。不是进程吃掉了整机,是整机内存耗尽,内核必须挑一个进程杀掉来续命。
命令对照表
| 观察 | dmesg 证据 | free 证据 |
|---|---|---|
| 有进程被杀 | Killed process 12345 (java) |
— |
| 整机内存耗尽 | — | available 60Mi,swap 已用 7.4Gi |
| 时间点吻合 | 02:13:41 击杀 |
监控告警时间一致 |
路径 — 下次遇到直接跑
三步定位命令组合

$ dmesg -T | grep -i "out of memory" # 第 1 步:确认是不是 OOM 击杀
$ free -h # 第 2 步:看被杀时刻整机内存水位
$ ps aux --sort=-%mem | head -5 # 第 3 步:看现在谁在吃内存(谁是下一个)
ps aux --sort=-%mem 按内存占用降序排进程,head -5 取前 5 名——这一步不是看过去的凶手,是看当前的内存大户,判断下一次 OOM 会轮到谁。三个命令在同一个 ssh 会话里连续执行,不需要装任何额外工具。
oom_score 判读:为什么杀的是它
找到记录后,如果你想看懂"为什么偏偏是它被选中",进进程目录看两个文件。注意:被杀的 12345 已经没了,我们看的是当前评分最高的同类进程——它就是下一次 OOM 的头号候选(下面这个是还活着的 report-service,PID 11092):
$ cat /proc/11092/oom_score
715
$ cat /proc/11092/oom_score_adj
0
oom_score:范围 0-1000,内核给进程的"死刑评分",越高越容易被杀。它的算法是(1000 + 进程内存占整机比例×1000) × 2/3——内存占得越多分越高,一个完全不占内存的普通进程也有约 666 的基准分。11092 现在占整机内存(物理 + swap)约 7%,得分 715;而刚才被杀的 12345 被杀时约占 14%,得分 763。分数没有固定危险线,全机最高就是头号候选——它俩都是全机最高档,谁先到顶谁先走。oom_score_adj:人工调整值,范围 -1000 到 1000,-1000 表示豁免(永远不会被杀),1000 表示强制首选击杀。默认 0,即不调整,全靠进程自身内存占用决定生死。
"下次遇到直接跑"的判断链条: 进程消失 → dmesg 找到 Killed process → 看 oom_score 多高 → 再回头看 ps aux 谁 RSS 最大。被杀的永远是"当前评分最高"的那个,而评分和内存占用强相关——Java 进程堆设得越大、吃得越多,越容易成为下一个祭品。
定位 — 内核的"死刑判决"是怎么下的
内核的决策:挑一个"代价最小"的进程杀掉
OOM Killer 不是随机杀进程,它遵循一个明确的内核决策过程。示意图如下:

当某个进程申请内存,而内核发现所有可回收的内存都腾不出来(page cache 回收完了、swap 也写满了、watermark 低于 min)时,它不会让系统死等,而是调用 OOM Killer:
- 遍历所有进程,按
oom_badness()打分。 - 打分依据:匿名内存(anon-rss)占大头,再加 swap 用量、页表占用——内存占得越多分越高。计分只看进程自身,不含子进程内存。
- 选评分最高的进程,发 SIGKILL。内核杀掉的是与该进程共享内存空间的整个线程组(比如同一个 JVM 的所有线程);独立的子进程不会被连带击杀,只会被 re-parent 给 systemd。
- oom_reaper 回收它的内存,系统继续运行。
这就是为什么 Java 总中招:-Xmx 设得越大,anon-rss 越高,oom_score 越高。在"整机内存不够"这个前提下,内核选的是"杀掉它腾出最多内存"的那个——Java 进程往往就是那个"最肥"的。所以 C2 的答案很清楚:这不是应用代码的 bug,而是内核在内存耗尽时,按"杀人成本最低"原则做的兜底决策。
overcommit:内存"承诺"比实际多
那为什么内存会突然"不够用"?一个常被忽略的根子在 overcommit(内存超卖)。查一下默认值:
$ cat /proc/sys/vm/overcommit_memory
0
overcommit_memory 是控制内核是否允许超卖内存的参数,三个取值对应三种策略:
| 值 | 模式 | 行为 |
|---|---|---|
| 0 | 启发式(默认) | 只拒绝明显过大的申请,其余先"点头承诺",物理内存不够时可能触发 OOM |
| 1 | 总是允许 | 从不拒绝 malloc,虚拟内存随便承诺——最激进,OOM 概率最高 |
| 2 | 禁止超卖 | 严格按 CommitLimit(物理 + swap 的 50%)限额,超了直接返回失败,不会 OOM,但 malloc 可能失败 |
进程 malloc 申请大块内存时,内核在模式 0/1 下不检查物理内存够不够就点头承诺。承诺比实际多,平时没事,可一旦真到物理内存用尽的瞬间,内核发现自己兑现不了承诺——于是只能触发 OOM Killer 杀进程补锅。
提示:想"杜绝 OOM"不是简单把值改成 2——那样大内存请求会直接失败,Java 启动都可能报错。模式 0 是多数发行版的理智默认,重点是管住"最肥的进程"(处方段)。
上篇我们看到的 MemAvailable 就是内核自己估算的"还能兑付多少"——它逼近 0,说明承诺已经快兑现不出来了。
这不是应用层的问题
这个案例属于系统层还是应用层? 是系统层。触发链条是:物理内存耗尽(系统层)→ 内核无法满足新的内存申请(系统层)→ OOM Killer 选择击杀(系统层)。应用代码本身没有 bug,它的日志干净得能当证据用。但这不代表应用完全无辜——Java 的堆设置(-Xmx)直接决定了它会不会成为"最肥的那个",这属于应用层的可调项,留到处方段处理。
处方
第一刀:把 JVM 堆上限调下来
既然被杀的是"最肥的进程",第一件事就是把 Java 进程的食量控制住。启动参数里显式设置堆上限:
$ java -Xms2g -Xmx4g -jar payment-service.jar
-Xms 是初始堆大小,-Xmx 是堆上限。生产环境必须显式设 -Xmx,且要比机器物理内存留出余量(至少给系统留 20%,上篇讲过 MemAvailable 才是真值)。比如 8GB 机器跑 Java,-Xmx 设 6g 以下更稳妥——否则它天生就是 oom_score 最高的候选。
第二刀:给关键进程 oom_score_adj 保护
有些进程你就是不能让它死(比如 MySQL、Kafka),用 oom_score_adj 给它"护身符":
$ echo -1000 > /proc/<pid>/oom_score_adj
-1000 是 OOM_SCORE_ADJ_MIN 最小值,等于给进程加"绝对豁免"——OOM Killer 永远不会把它选为目标。要分清两个 sysctl:vm.oom_kill_allocating_task 默认 0(允许杀任意高分进程),而 vm.panic_on_oom 设为 2 时内核干脆直接 panic 不执行击杀(生产一般设 0 让它杀进程续命)。反过来,如果你确定某个进程可以牺牲,可以设正值让它优先被选。生产上典型做法:关键中间件设 -500 ~ -1000,Java 应用按重要性设 0 或负值。
⚠️ 注意:echo 写进 /proc 是临时的,进程一重启就失效。 生产上要用 systemd 持久化:
# /etc/systemd/system/mysql.service 的 [Service] 段
[Service]
OOMScoreAdjust=-500
OOMScoreAdjust 是 systemd 的原生配置项,等价于启动后自动把该进程的 oom_score_adj 设为对应值(无需手动写 /proc)。改完 systemctl daemon-reload && systemctl restart mysql 生效。这才是能扛住重启的保护,echo 只是临时探路用的。
第三刀:监控前置,别等被杀
最优雅的解决是让 OOM 根本不发生。上篇讲过 MemAvailable 是判断内存够不够的真值,这里把它变成告警:
- 阈值:
MemAvailable / MemTotal < 10%持续 5 分钟 → 严重告警(濒临 OOM)。 - 配合 swap 观察:swap 使用率持续上升 = 内存真的不够,不是 page cache 可回收的假象。
- 击杀后必看:一旦 dmesg 出现
Killed process,立刻跑上面的三步命令组合,把内存大户揪出来,调堆或加内存,否则明晚同一时刻再杀一次。
三刀速查表
| 场景 | 手段 | 一句话 |
|---|---|---|
| 别让 Java 当"最肥的" | -Xmx 设堆上限 |
堆越大、oom_score 越高,越容易中招 |
| 保护不能死的中间件 | systemd OOMScoreAdjust=-500 |
内核杀人时跳过它们 |
| 让 OOM 根本不发生 | MemAvailable 告警 | 阈值 10%,提前 5 分钟动手 |
记住这个命令组合
$ dmesg -T | grep -i "out of memory" # 进程莫名消失 → 先确认是不是 OOM 击杀
$ cat /proc/<pid>/oom_score # 看进程死刑评分(全机最高就是头号候选)
$ cat /proc/<pid>/oom_score_adj # 看是否被人工调整过
$ ps aux --sort=-%mem | head -5 # 看现在谁在吃内存,谁是下一个
附:完整命令清单
dmesg -T | grep -i "out of memory" # 找 OOM Killer 击杀记录
free -h # 看整机内存水位(available 列)
cat /proc/sys/vm/overcommit_memory # 看超卖策略(0=启发式)
cat /proc/<pid>/oom_score # 看进程死刑评分(0-1000)
cat /proc/<pid>/oom_score_adj # 看人工调整值(-1000 豁免)
echo -1000 > /proc/<pid>/oom_score_adj # 给关键进程加保护(临时生效)
ps aux --sort=-%mem | head -5 # 找内存大户
内核版本说明:
/proc/sys/vm/overcommit_memory、/proc/<pid>/oom_score、oom_score_adj自 2.6 内核起稳定存在。4.6 内核引入了oom_reaper收割线程(在此之前,被杀进程的内存靠系统后续缓慢回收)。cgroup v2 环境下的 OOM 行为有差异(memory.max 触发的是 cgroup 内 OOM,不经过全局 oom_score),下篇专门讲。
阅读路线
内存类排查共 9 篇,这篇是 OOM Killer 场景的案例篇。下篇我们聊 容器中 Java 进程被 killed 但 dmesg 什么都没——memory cgroup 排查:当 Java 跑在容器里被 OOM,dmesg 里却找不到击杀记录时,日志去哪了?cgroup v2 的 OOM 又该怎么定位。
记住一个心法:Linux 不会无缘无故杀掉你的进程——OOM Killer 只是在内存耗尽时,替你选了一个"最该牺牲"的进程。 排查"进程莫名消失",第一步永远是 dmesg 找击杀记录,而不是重启重试。判断方向比跑命令更重要:进程没了先问"是崩溃还是被杀",这个问题的答案决定你往应用层还是系统层查。