2024 年的一个下午,一个跑在 8G 内存机器上的 Java 服务,毫无征兆地"消失"了。我说的"消失",是字面意义上的:进程没了,端口不通了,但我翻遍了应用自己的日志,从头到尾找不到一行异常,没有栈、没有 OOM、没有任何报错——日志就在某一行的中间,戛然而止,后面什么都没有了。我第一反应是程序自己崩了,八成是哪里有 bug、或者 JVM 出了问题。我反复看那段日志的最后几行,正常得不能再正常,就是普通的业务日志,然后下一秒,服务就不在了。一个程序如果是自己崩的,它多少会留下点"遗言"——一个异常栈,一句 error,哪怕一行 core dump 的提示。可这个服务,死得干干净净,安安静静,像是被人从背后一刀,连哼都没来得及哼一声。我盯着那行被硬生生截断的日志想了很久:如果不是它自己崩的,那是谁,在它毫不知情的情况下,把它杀掉的?这件事逼着我把 Linux 的内存机制、OOM Killer、内存 overcommit 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,8G 内存的机器,跑着一个 Java 服务 + 若干其他进程
事故现象:
- Java 服务进程突然消失,端口不通
- ★ 应用自己的日志没有任何异常,日志在某一行中间断掉
- 没有 core dump,没有 error,服务像凭空蒸发
现场排查:
# 1. 应用日志 —— 干净得反常
$ tail -50 /opt/app/logs/app.log
... 2024-xx-xx 14:22:07 处理订单 88231 # ★ 最后一行,正常业务日志
$ # 后面什么都没有了
# 2. ★ 翻内核日志 dmesg —— 真相在这里
$ dmesg -T | grep -i 'out of memory'
[xxx] Out of memory: Kill process 12345 (java) score 902 or sacrifice child
[xxx] Killed process 12345 (java) total-vm:7.2g, anon-rss:6.1g # ★ 元凶!
# ★ 不是程序崩了 —— 是内核把它【杀】了。
# 3. 看当时的内存情况
$ free -h
total used free shared buff/cache available
Mem: 7.6G 7.1G 180M 20M 320M 220M # ★ available 只剩 220M
根因(后来想清楚的):
1. ★ 这个进程不是自己崩的 —— 是 Linux 内核里一个叫
【OOM Killer】(Out Of Memory Killer)的机制杀的。
2. 当物理内存 + swap 都快用光、内核又回收不出足够
内存时,它为了不让【整个系统】卡死/崩溃,会【强行
挑一个进程杀掉】,腾出内存来保命。
3. ★ 杀进程用的是 SIGKILL(信号 9)—— 这个信号
【不可捕获、不可忽略】。进程没有任何机会做收尾,
包括【写一行日志】—— 这就是应用日志为什么是空的。
4. OOM Killer 挑谁杀,看一个叫 oom_score 的分,
分大致和【进程占的内存】成正比 —— 占内存最多的,
往往就是【主服务自己】,于是它第一个被杀。
5. 内存为什么会满到这一步:Linux 默认【超额承诺】
(overcommit)内存,加上这台机器本身就没配 swap、
内存也确实偏紧,几个进程一挤,就到了临界点。
6. 应用日志没异常,不是没事 —— 是它根本没机会留遗言。
进程凭空消失 + 应用日志干净 = 先去 dmesg 找 OOM。
修复 1:进程凭空消失,别只翻应用日志——看 dmesg
# === ★ 先纠正最核心的误解:它不是"自己崩的" ===
# === 一个关键线索:应用日志"干净"本身就不正常 ===
# 一个程序如果是【自己】出问题挂掉的(空指针、OOM
# 异常、段错误),它多少会留下痕迹:
# - Java 会打异常栈、可能打 OutOfMemoryError
# - C/C++ 程序段错误会留 core dump
# - 至少日志会有一行 error
# ★ 如果日志【干干净净、在某行中间硬断】—— 这恰恰说明
# 进程是被【从外部、瞬间】干掉的,没机会做任何收尾。
# === ★ 谁能"瞬间干掉"一个进程:SIGKILL ===
# kill -9(SIGKILL)这个信号很特殊:
# - 进程【无法捕获】它、无法忽略它、无法注册处理函数
# - 内核收到就【立即】终止进程,不给它任何执行机会
# ★ 所以被 SIGKILL 杀的进程,连"我要死了"这行日志
# 都写不出来 —— 它的死,在它自己的日志里是【隐形】的。
# === ★ 那是谁发的 SIGKILL —— 内核的 OOM Killer ===
# 真相不在应用日志里,在【内核日志】里。查这两个地方:
$ dmesg -T | grep -i -E 'out of memory|killed process|oom'
$ journalctl -k | grep -i -E 'out of memory|oom' # systemd 系统
# ★ -T 让 dmesg 显示人类可读的时间。
# === 读懂 OOM 的那几行日志 ===
# [时间] Out of memory: Kill process 12345 (java) score 902 ...
# [时间] Killed process 12345 (java) total-vm:7.2g, anon-rss:6.1g
# 逐项看:
# - Kill process 12345 (java) :被杀的进程 PID 和名字
# - score 902 :★ 它的 oom_score,越高越该死
# - total-vm:7.2g :它的虚拟内存大小
# - anon-rss:6.1g :★ 它实际占的物理内存 —— 6.1G!
# ★ 这一行就把"谁、占了多少、为什么被选中"全说清了。
# === OOM 还会打印一张"案发现场的内存表" ===
$ dmesg -T | grep -A30 'invoked oom-killer'
# OOM 触发时,内核会把当时【所有进程】的内存占用列一张表,
# 每行有 pid、rss、oom_score_adj、名字。
# ★ 这张表是最值钱的现场证据 —— 谁在吃内存,一目了然。
修复 2:内存为什么会"满"——物理内存、cache 与 overcommit
# === ★ 把"内存满了"这件事彻底讲清楚 ===
# === free -h 每一列到底是什么 ===
$ free -h
total used free shared buff/cache available
Mem: 7.6G 7.1G 180M 20M 320M 220M
# - total :物理内存总量
# - used :进程实打实用掉的
# - free :★ 完全没被碰过的 —— 这个数【会骗人】
# - buff/cache :内核拿空闲内存做的磁盘缓存
# - available :★★ 真正"还能给进程用"的内存 —— 看这个!
# === ★ 关键:别看 free,要看 available ===
# 很多人看到 free 只剩 180M 就慌,或看到 free 还多就放心 ——
# ★ 都错。free 是"一点没碰过"的,但 buff/cache 里有
# 很大一部分是【可以随时回收】给进程用的。
# available = free + 可回收的 cache,★ 它才是真实余量。
# 这次 available 只剩 220M —— 这才是"内存真的快没了"。
# === buff/cache 不是总能救场 ===
# cache 大多可回收,但有些回收【不动】或【回收不及】:
# - 脏页(改过还没写回磁盘的)要先落盘才能回收
# - 进程在飞快申请内存时,回收速度可能跟不上
# ★ 所以"有 cache"不等于"内存安全"。
# === ★ overcommit:Linux 会"超额承诺"内存 ===
# 进程申请内存(malloc),内核往往【先答应下来】,
# 并【不立即】真的分配物理内存 —— 等进程真正去写,
# 才分配。这叫 overcommit(超额承诺)。
# ★ 内核赌的是:不是所有进程都会同时用满它申请的量。
# 这个赌大多数时候赢 —— 但万一大家真同时用起来,
# 承诺的总量 > 物理内存,内存就【真的】不够了。
# === 看 overcommit 策略 ===
$ cat /proc/sys/vm/overcommit_memory
0
# 0 = 启发式超额承诺(默认);1 = 无脑答应;
# 2 = 严格,按 总量 不超额。
# === ★ swap:内存的"最后一道缓冲" ===
$ free -h | grep -i swap
Swap: 0B 0B 0B # ★ 这台机器没配 swap!
# swap 是把内存里"暂时不用的页"挪到磁盘,腾出物理内存。
# ★ 没有 swap,内存一紧就【没有任何缓冲】,直接撞 OOM。
# 这台机器没 swap,是它这么容易 OOM 的一个推手。
修复 3:OOM Killer 怎么挑"谁该死"——oom_score
# === ★ 内存真不够了,内核怎么决定杀谁 ===
# === 每个进程都有一个"该死指数":oom_score ===
$ cat /proc/12345/oom_score
902
# ★ 每个进程都有一个 oom_score。内存耗尽、必须杀人时,
# 内核就挑【oom_score 最高】的那个进程下手。
# === oom_score 大致怎么算 ===
# 核心:★ 一个进程占的【物理内存】越多,它的 oom_score
# 越高 —— 因为杀它能腾出最多内存,"性价比"最高。
# 还会参考运行时长、是不是 root 进程等,但内存占用是大头。
# ★ 残酷的结论:占内存最多的进程最先死 —— 而占内存最多的,
# 常常正是你最想保住的【主服务】。这次的 Java 就是。
# === ★ oom_score_adj:人为调整"该死指数" ===
$ cat /proc/12345/oom_score_adj
0
# oom_score_adj 范围 -1000 ~ +1000,它会【加到】评分上:
# - 调成负数(如 -500)-> 总分降低 -> 更【不容易】被杀
# - ★ 调成 -1000 -> 几乎【永不】被 OOM Killer 杀
# - 调成正数 -> 更【容易】被杀(让它当炮灰)
# === 给一个正在跑的进程调低"该死指数" ===
$ echo -800 > /proc/12345/oom_score_adj
# ★ 把关键服务的 oom_score_adj 调低,内存紧张时
# 内核会【优先杀别的进程】,保住它。
# 注意:这是【临时】的,进程重启就没了(持久化见修复 5)。
# === "or sacrifice child" 是什么意思 ===
# OOM 日志里常有 "Kill process X or sacrifice child"。
# ★ 意思是:内核选中了进程 X,但如果 X 有子进程,
# 它可能改杀某个【子进程】—— 杀子进程一样能腾内存,
# 但对主进程"伤害"小一点。
# === 看系统全局的 OOM 行为开关 ===
$ cat /proc/sys/vm/panic_on_oom
0
# 0 = OOM 时杀进程(默认,系统继续活);
# 1 = OOM 时直接 panic 整机重启(某些场景才用)。
修复 4:当场定位是谁吃光了内存
# === ★ 事后看 dmesg,事中/复现看实时内存 ===
# === 事后:dmesg 已经写明了一切 ===
$ dmesg -T | grep -A30 'invoked oom-killer'
# OOM 现场那张进程内存表,每行大致是:
# [ pid ] uid tgid total_vm rss ... oom_score_adj name
# ★ rss 那列从大到小看,排第一的就是吃内存的大户。
# 配合 "Killed process" 那行,就知道谁吃光的、谁被杀的。
# === ★ 实时:按内存占用给进程排序 ===
$ ps aux --sort=-rss | head
# USER PID %CPU %MEM VSZ RSS ... COMMAND
# app 12345 40 78.0 7.2g 6.1g ... java ... # ★ RSS 6.1G,绝对大户
# ★ --sort=-rss:按物理内存(RSS)倒序。第一行就是嫌疑犯。
# === top 里实时看(交互式)===
$ top
# 进 top 后按 大写 M —— 按内存占用排序。
# ★ 看 RES 列(常驻内存),和 ps 的 RSS 是一回事。
# === 看整机内存的细账 ===
$ cat /proc/meminfo | head -20
MemTotal: 7.6G
MemAvailable: 220M # ★ 真实可用,和 free 的 available 一致
Cached: ...
SwapTotal: 0 # ★ 没 swap
# ★ MemAvailable 持续很低 = 内存长期紧张,迟早 OOM。
# === ★ 持续盯着内存走势,判断"泄漏 还是 不够" ===
$ watch -n5 'free -h; echo; ps aux --sort=-rss | head -5'
# 两种情况要分清:
# - 某个进程 RSS【只涨不跌】,业务空了也不降 -> 内存泄漏,
# 要交给开发改代码。
# - 所有进程内存都正常,只是【加起来就是超过 8G】
# -> 整机内存不够,要限内存 / 加内存(修复 5)。
# === 看某个进程的内存明细(确认是不是它泄漏)===
$ cat /proc/12345/status | grep -E 'VmRSS|VmSize'
VmRSS: 6.1 GB # ★ 实际物理内存
VmSize: 7.2 GB # 虚拟内存
修复 5:怎么从根上避免 OOM
# === ★ 杀进程是内核的"止损",真正的修复在内存治理 ===
# === ★ 方向 1:给每个服务【设内存上限】—— 最关键 ===
# OOM 的根因常是某个服务【不受约束地】吃内存,把整机拖垮。
# 正解是给它【套个笼子】,让它最多只能用这么多。
# Java 服务:务必显式设堆上限,别让它无限涨
$ java -Xmx2g -jar app.jar
# ★ -Xmx2g:JVM 堆最多 2G。不设的话 JVM 可能按机器
# 内存的 1/4 自行决定,几个服务一叠加就爆。
# ★ 用 systemd 给服务设内存硬上限(强烈推荐)
$ vi /etc/systemd/system/myapp.service
[Service]
MemoryMax=3G # ★ 这个服务最多用 3G
MemoryHigh=2.5G # 软上限,超了就加压回收
$ systemctl daemon-reload && systemctl restart myapp
# ★ 这样即使服务想吃更多,cgroup 也会先限制/回收它,
# 把"它一个把整机拖垮"变成"它自己被限制住"。
# === 方向 2:给关键进程持久化调低 oom_score_adj ===
# systemd 服务可以直接在 unit 里配:
$ vi /etc/systemd/system/critical.service
[Service]
OOMScoreAdjust=-800 # ★ 内存紧张时,内核优先杀别的
# ★ 这是"保命"手段,不能解决内存不够 —— 别滥用。
# === ★ 方向 3:配 swap,给内存一个缓冲垫 ===
# 这台机器没 swap,内存一紧就直接 OOM。加一块 swap:
$ fallocate -l 4G /swapfile
$ chmod 600 /swapfile
$ mkswap /swapfile && swapon /swapfile
$ echo '/swapfile none swap defaults 0 0' >> /etc/fstab
# ★ swap 不是"解决"内存不够,是给系统一个【喘息缓冲】——
# 不至于内存一抖就立刻杀进程。但 swap 太频繁会拖慢系统。
# === 方向 4:监控 available 内存,别等 OOM ===
$ free -m | awk 'NR==2{print $7}' # 取 available(MB)
# ★ 把 MemAvailable 纳入告警 —— 它持续低于某条线就报警,
# 在 OOM 之前就介入,而不是等服务死了才从 dmesg 里捡尸。
# === 方向 5:确实不够,就加内存 / 分流 ===
# 如果所有服务都已经限了内存、加起来还是超过物理内存 ——
# ★ 那就是机器本身配置不够。要么升配,要么把服务拆到
# 别的机器上。这种情况下,任何"技巧"都只是拖延。
# === ★ 治理优先级 ===
# 1. 先给每个服务设内存上限(-Xmx / MemoryMax)—— 根上的事
# 2. 关键服务调低 oom_score_adj —— 保命
# 3. 配 swap —— 加缓冲
# 4. 监控 available —— 提前预警
# 5. 真不够 —— 加内存。
修复 6:OOM 与内存排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 进程凭空消失 + 应用日志干净 = 先查 dmesg ===
$ dmesg -T | grep -i -E 'out of memory|killed process'
# 被 SIGKILL 杀的进程留不下遗言,真相在内核日志里。
# === 2. ★ 看内存余量看 available,不要看 free ===
$ free -h # available 才是真实可用,free 会骗人
# === 3. OOM Killer 按 oom_score 杀人,占内存最多的先死 ===
# 而占内存最多的,常常就是你最想保住的主服务。
# === 4. ★ 定位吃内存大户:按 RSS 排序 ===
$ ps aux --sort=-rss | head
$ dmesg -T | grep -A30 'invoked oom-killer' # 事后看现场内存表
# === 5. 分清:某进程泄漏,还是整机内存就是不够 ===
$ watch -n5 'free -h; ps aux --sort=-rss|head -5'
# 只涨不跌 = 泄漏改代码;都正常但加起来超 = 限内存/加内存。
# === 6. ★ 每个服务都要设内存上限 ===
# JVM 设 -Xmx;systemd 服务设 MemoryMax。别让谁裸奔。
# === 7. 关键服务配 OOMScoreAdjust 调低,但别滥用 ===
# 这是保命,不是解决内存不够。
# === 8. 排查"进程被 OOM 杀"的命令链 ===
$ dmesg -T | grep -i 'out of memory' # ① 确认是不是 OOM
$ dmesg -T | grep -A30 'invoked oom' # ② 看现场内存表
$ free -h # ③ 看当前内存余量
$ ps aux --sort=-rss | head # ④ 找吃内存大户
$ 给服务设 -Xmx / MemoryMax # ⑤ 套上内存笼子
# 按这个顺序,OOM 基本能定位、能根治。
命令速查
需求 命令
=============================================================
确认进程是不是被 OOM 杀 dmesg -T | grep -i 'out of memory'
看 OOM 现场的进程内存表 dmesg -T | grep -A30 'invoked oom-killer'
看真实可用内存 free -h (看 available 列)
按物理内存排序找大户 ps aux --sort=-rss | head
top 里按内存排序 top 然后按大写 M
看某进程的 OOM 评分 cat /proc/PID/oom_score
临时调低进程的 OOM 评分 echo -800 > /proc/PID/oom_score_adj
看内存细账 cat /proc/meminfo
JVM 设堆上限 java -Xmx2g -jar app.jar
systemd 服务设内存上限 [Service] MemoryMax=3G
systemd 服务调低 OOM 评分 [Service] OOMScoreAdjust=-800
口诀:进程凭空消失应用日志干净,先 dmesg 找 Out of memory
内存余量看 available 不看 free,每个服务都要设内存上限
避坑清单
- 进程凭空消失而应用日志没有任何异常,日志在某行中间硬断,八成是被外部杀掉的
- OOM Killer 用 SIGKILL 杀进程,这个信号不可捕获,进程没机会写任何遗言日志
- 真相不在应用日志里,在内核日志,用 dmesg -T 或 journalctl -k 找 Out of memory
- 看内存余量要看 available 不是 free,free 只是没碰过的,buff/cache 有大量可回收
- Linux 默认超额承诺内存,申请时先答应不真分配,大家真用起来就可能不够
- OOM Killer 按 oom_score 挑进程杀,分数和占用内存成正比,占内存最多的先死
- 占内存最多的常常就是主服务自己,所以 OOM 第一个被杀的往往是你最想保的
- oom_score_adj 可调进程被杀的优先级,调成 -1000 几乎永不被杀,但这只是保命
- 根上的修复是给每个服务设内存上限,JVM 用 -Xmx,systemd 服务用 MemoryMax
- 没配 swap 内存一紧就直接 OOM,配 swap 是缓冲,监控 available 才能提前预警
总结
这次"服务进程凭空消失、应用日志却干干净净"的事故,纠正了我一个关于"程序之死"的、想当然的预设。在我的认知里,一个程序如果死了,它一定是"自己"死的——要么是踩中了一个 bug,要么是内存溢出抛了异常,要么是哪段逻辑算崩了。总之,死因一定在它自己身上,而它临死前,也一定会在自己的日志里,留下一行半句的遗言:一个异常栈,一句 error,哪怕是一行没头没尾的报错。正因为我笃定"死因在它自己、遗言在它日志里",当我翻遍那份日志、却发现它平静得反常——平静到在一行普通的业务日志中间,毫无征兆地就断掉了——我整个人是懵的。我把那份"干净"的日志,误读成了"没有线索"。而事实恰恰相反:那份诡异的干净,本身就是最大的线索。它在无声地告诉我:这个进程,根本没有机会留下遗言。它不是"自己"死的,它是被人从背后,一击毙命。复盘到根上,我才真正认识了那个"凶手"——Linux 内核里一个叫 OOM Killer 的机制。当整台机器的内存被挤到悬崖边、内核再也腾不出空间时,它必须做一个冷酷的决定:与其让整个系统一起陷入瘫痪,不如选一个进程,把它杀掉,用它的尸体换回所有进程的生路。而它选中我那个 Java 服务的理由,冷酷得近乎讽刺——因为它占的内存最多,杀它,腾出的空间最大,"性价比"最高。它动手用的,是 SIGKILL 这把最锋利的刀:这个信号不可捕获、不可阻挡,内核手起刀落,进程在那一瞬间就停止了存在,它甚至来不及反应过来发生了什么,自然也来不及往日志里写下哪怕一个字。所以那行日志不是断在某个 bug 上,它是断在了那一刀上。真正的死亡证明,从来不在它自己的日志里,而在内核的日志——dmesg——里,清清楚楚写着:Out of memory,Killed process。这次最大的收获,是我意识到,排查问题时,我太习惯于"在出问题的那个东西自己身上找原因"了。服务挂了,我就翻服务的日志;接口报错,我就盯接口的代码。这个习惯大多数时候没错,但它有一个致命的盲区:它默认了"问题的原因,一定在问题的表现之处"。可一个进程,它从来不是孤立地运行的——它活在一个由内核统一调度、统一仲裁资源的系统里。当系统层面的资源(内存、fd、inode)亮起红灯时,内核会越过这个进程的"意志",直接对它执行处置。这种死亡,在进程自己的视角里是完全不可见的、是没有理由的。所以下一次,当一个进程死得"没有理由"时,我会提醒自己:也许理由不在它身上,而在那个俯视着它、也俯视着所有进程的内核手里。先抬起头,去看看内核的日志——它往往早就把判决书,写得明明白白了。
—— 别看了 · 2026