2024 年,我们一个 Java 服务开始闹鬼。它会在半夜某个时刻突然消失——进程没了,端口也没了,监控发来告警。我第一时间扒应用日志,日志结尾平平静静,最后几行是再正常不过的业务输出,没有任何异常、没有任何报错、没有任何"我要退出了"的迹象,就像一个人说着话突然被人从中间掐断。重启,服务又活蹦乱跳;过几天,又在半夜悄无声息地没了。一个进程,既没崩溃报错,也没收到我发的关闭信号,却凭空蒸发——这事透着一股说不出的诡异。我盯着那截戛然而止的日志看了很久,始终想不通:到底是谁,在半夜杀了我的进程,还杀得这么干净、不留一句话?后来才发现,凶手根本不在我的应用日志里,而在内核日志里——是 Linux 内核的 OOM Killer。这件事逼着我把 Linux 的内存管理、free、swap、OOM Killer 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,8G 内存,跑着一个 Java 服务和若干杂项
事故现象:
- Java 服务半夜进程凭空消失,端口也没了
- 应用日志结尾完全正常,无任何报错、无退出日志
- 重启后正常,过几天又在半夜消失
现场排查:
# 1. 应用日志结尾,平静得反常
$ tail -20 /var/log/app/app.log
... 处理订单 ORD2024xxx 完成
... 处理订单 ORD2024yyy 完成
(日志到此为止,没有任何异常或退出信息)
# ★ 进程不是自己退的,是被【外力】终止的
# 2. ★ 关键命令:看内核日志 dmesg
$ dmesg -T | grep -i 'killed process\|out of memory'
[Sun May 19 03:14:22 2024] Out of memory: Kill process 8123 (java)
score 879 or sacrifice child
[Sun May 19 03:14:22 2024] Killed process 8123 (java)
total-vm:9123456kB, anon-rss:6234567kB
# ★ 真相大白:是内核的 OOM Killer 杀了 java 进程!
# 3. 看内存到底紧张到什么程度
$ free -h
total used free shared buff/cache available
Mem: 7.6G 6.9G 180M 12M 520M 210M
Swap: 0B 0B 0B
# ^^^^^^
# ★ available 只剩 210M;而且 Swap 是 0 —— 一点缓冲都没有
根因(后来想清楚的):
1. ★ 物理内存被吃满:Java 堆 + 其他进程,把 8G 几乎用尽,
available 只剩两百多兆。
2. ★ 这台机器【没有配置 swap】(Swap: 0B)。内存一旦告急,
连"把冷数据换到磁盘救急"这条退路都没有。
3. ★ 当某个时刻内存彻底不够分,内核为了自保,会启动
【OOM Killer】—— 挑一个"罪魁祸首"进程直接杀掉,
释放内存,保住整个系统不崩。
4. 它的打分规则,倾向于杀【占内存最多】的进程 ——
于是吃内存大户 Java 进程,成了首选目标。
5. ★ OOM Killer 是【内核】干的,用的是 SIGKILL,
进程没有一丝机会留下遗言 —— 这就是应用日志为何那么"干净"。
进程半夜凭空消失,先去 dmesg 里找 OOM Killer。
修复 1:free 命令——内存到底用掉了多少
# === ★ 排查内存,第一个命令永远是 free ===
$ free -h # -h = 人类可读单位
total used free shared buff/cache available
Mem: 7.6G 6.9G 180M 12M 520M 210M
Swap: 0B 0B 0B
# === 逐列看懂(★ 每一列都有人理解错)===
# total 物理内存总量
# used 已被【进程】真正使用的内存
# free 完全【空闲、谁也没碰】的内存
# shared 多进程共享的内存(tmpfs 等)
# buff/cache 内核拿去做【缓冲和缓存】的内存(下一节专讲)
# available ★ 真正重要的一列:估算【还能给新进程用】多少
# === ★ 最大的误区:盯着 free 那一列,以为它就是"可用内存" ===
# free = 纯空闲。但 buff/cache 那部分,内存一紧张【随时能还】。
# ★ 所以"还能用多少",看的是 available,不是 free!
# 这台机器 free 只有 180M 看着吓人,
# 但真正的紧张程度由 available(210M)说了算 —— 同样很危险。
# === 看更细的内存分布 ===
$ cat /proc/meminfo # 内核给出的完整内存信息
$ cat /proc/meminfo | grep -iE 'MemTotal|MemFree|MemAvailable'
# === 看【哪个进程】吃内存最多 —— 揪出大户 ===
$ ps aux --sort=-%mem | head -6
USER PID %MEM RSS COMMAND
root 8123 78.2 6234567 java -Xmx6g ... # ★ 一个进程吃了 78%
# RSS = 该进程实际占用的物理内存(单位 KB)。
$ top -o %MEM # top 里按内存排序,动态看
# === ★ 一个常被忽略的点:看 RSS 也要看进程自己的"上限" ===
# 比如 Java,-Xmx6g 就给了它 6G 堆 —— 它本来就奔着吃 6G 去的。
# 一台 8G 的机器塞一个 -Xmx6g 的 Java,本身就埋了雷。
修复 2:buff/cache 不是"被占用"——available 才是真相
# === ★ 这是关于 Linux 内存,最该破除的一个误解 ===
# === 现象:很多人第一次看 free,会被 free 列吓到 ===
$ free -h
total used free buff/cache available
Mem: 7.6G 2.1G 200M 5.3G 5.0G
# "free 只剩 200M 了!内存要爆了!" —— ★ 这是错的判断。
# === buff/cache 到底是什么 ===
# Linux 有个理念:【空闲的内存就是浪费的内存】。
# 所以内核会主动把暂时没人用的内存,拿去缓存最近读过的
# 文件、磁盘块 —— 这就是 buff/cache。目的是【下次读得更快】。
# ★ 关键:这部分内存是"借用"。一旦有进程真的需要内存,
# 内核会立刻把 buff/cache 释放还回去。
# === ★ 所以判断内存够不够,只看一列:available ===
# available = free + 【可以被回收的】buff/cache 等
# = 内核估算的"现在还能给新进程多少内存"
# 上面例子:free 才 200M,但 available 有 5.0G ——
# 真实情况是【内存很宽裕】,那 5.3G cache 随叫随还。
# === ★ 反过来,真正危险的信号是这个 ===
# available 很低(比如只剩几百兆)+ free 也低
# -> 这才是内存真的快不够了。我这次的事故,正是这种:
# available 只剩 210M,cache 也压到很低,没东西可还了。
# === 手动释放 cache(★ 一般【不需要】,生产慎用)===
$ sync && echo 3 > /proc/sys/vm/drop_caches
# ★ 这只是"看着舒服"—— cache 本来就会自动释放,
# 手动 drop 反而丢了缓存、让后续读变慢。除非排查需要,别动。
# === 一句话纪律 ===
# ★ 看内存够不够,看 available;别被 free 那一列吓住。
# buff/cache 高,通常是好事,说明内核在好好利用内存。
修复 3:swap 是什么——内存的"备胎"
# === ★ 这次事故的一个加重因素:这台机器 Swap 是 0 ===
# === swap 是什么 ===
# swap(交换空间)是【磁盘上】划出的一块区域,
# 当物理内存吃紧时,内核会把一些【不活跃】的内存页
# (一阵子没被访问的)挪到 swap 里,腾出物理内存救急。
# ★ 它是内存的"备胎"——慢,但能在内存告急时多撑一会儿。
# === 看当前 swap 情况 ===
$ free -h | grep Swap
Swap: 0B 0B 0B # ★ 这台机器没配 swap
$ swapon --show # 看有哪些 swap 设备/文件
# 没输出 = 一个 swap 都没有。
# === ★ 没有 swap 意味着什么 ===
# 内存一旦不够,内核【没有缓冲余地】——
# 不能"挪点冷数据去磁盘撑一下",只能立刻走极端:OOM Killer。
# 有 swap 时,内存紧张会先表现为"变慢"(在 swap 里换进换出),
# 给你留出发现问题、处理问题的时间窗口。
# === 给机器加一块 swap(用文件做,简单)===
$ dd if=/dev/zero of=/swapfile bs=1M count=4096 # 建 4G 文件
$ chmod 600 /swapfile # ★ 权限必须 600
$ mkswap /swapfile # 格式化成 swap
$ swapon /swapfile # 启用
$ free -h | grep Swap # 确认生效
Swap: 4.0G 0B 4.0G
# === ★ 让 swap 开机自动生效:写进 /etc/fstab ===
$ echo '/swapfile none swap defaults 0 0' >> /etc/fstab
# 不写这行,重启后 swap 就没了。
# === ★ swap 不是万能药,要清醒认识 ===
# - swap 在磁盘上,比内存慢几个数量级。
# 服务大量用 swap = 性能雪崩,本质还是内存不够。
# - swap 的价值是【缓冲和兜底】,不是"内存的廉价替代品"。
# - 真正的解法永远是:加物理内存 / 减少程序内存占用。
# swap 只是让你在出事前,多一个"变慢"的预警信号。
修复 4:OOM Killer——谁在半夜杀了我的进程
# === ★ 这次的真凶:OOM Killer(Out Of Memory Killer)===
# === 它是什么、为什么存在 ===
# 当内核发现"内存彻底不够分了,再不处理整个系统都要卡死",
# 它会启动 OOM Killer:【挑一个进程杀掉】,释放内存,
# 保住整个系统不崩。这是内核的【自保机制】。
# ★ 它用的是 SIGKILL —— 被选中的进程没有一丝机会清理、
# 没有一丝机会留下日志。这就是应用日志为何那么"干净"。
# === ★ 怎么确认进程是被 OOM Killer 杀的 ===
$ dmesg -T | grep -i 'out of memory\|killed process'
[Sun May 19 03:14:22 2024] Out of memory: Kill process 8123 (java)
[Sun May 19 03:14:22 2024] Killed process 8123 (java) total-vm:...
# 或从 systemd 日志看:
$ journalctl -k | grep -i 'oom\|killed process'
$ journalctl --since '2024-05-19 03:00' | grep -i oom
# ★ 应用日志里找不到的死因,内核日志 dmesg 里写得清清楚楚。
# === OOM Killer 怎么挑"该杀谁" —— oom_score ===
# 内核给每个进程算一个 oom_score,分越高越容易被杀。
# 算分主要看:进程占了多少内存(占得越多分越高)。
$ cat /proc/8123/oom_score # 看某进程的当前得分
# ★ 所以"吃内存大户"永远是 OOM Killer 的首选目标 ——
# 我那个 -Xmx6g 的 Java,几乎是必选项。
# === ★ 保护关键进程:调 oom_score_adj ===
# oom_score_adj 范围 -1000 ~ 1000,负数 = 降低被杀概率。
$ echo -500 > /proc/8123/oom_score_adj # 让 8123 不那么容易被杀
$ echo -1000 > /proc/8123/oom_score_adj # -1000 = 基本豁免
# ★ 但这只是"换个进程杀"——内存的总账不平,迟早还是要出事。
# systemd 服务里也能配:[Service] OOMScoreAdjust=-500
# === ★ 别忘了:被杀的不一定"该死" ===
# OOM Killer 杀的是"占内存最多的",未必是"真正泄漏的那个"。
# 你的核心服务因为内存大,可能替真正的内存元凶背了锅。
# 所以查到 OOM,别只盯着被杀的进程,要查【整机内存为何耗尽】。
修复 5:overcommit 与 swappiness——两个相关内核参数
# === ★ 围绕 OOM,有两个内核参数值得了解 ===
# === 参数 1:vm.overcommit_memory —— 内核"超额承诺"内存 ===
$ sysctl vm.overcommit_memory
vm.overcommit_memory = 0 # 默认值
# Linux 默认允许"超额承诺":进程申请内存(malloc)时,
# 内核往往【先答应下来】,赌"你不会真的全用上"。
# - 0:启发式判断,适度超额(默认,绝大多数场景用这个)
# - 1:无脑答应所有申请(★ 危险,极易触发 OOM)
# - 2:严格模式,按 swap + 物理内存×比例 算总额,不超额
# ★ 一般【不要乱改】这个值,默认的 0 适合绝大多数情况。
# 它解释了一个现象:为什么进程 malloc 时没报错,
# 等真正去【写】那块内存时,反而触发了 OOM。
# === 参数 2:vm.swappiness —— 内核多积极地用 swap ===
$ sysctl vm.swappiness
vm.swappiness = 30 # 0~100,越大越爱用 swap
# - 值大:内存还没很紧张,内核就开始往 swap 挪页
# - 值小:尽量用物理内存,实在不行才动 swap
# 服务器一般设小一点(10~30),减少不必要的换页、保性能:
$ vi /etc/sysctl.conf
vm.swappiness = 10
$ sysctl -p
# ★ 注意:swappiness=0 不等于"禁用 swap"——
# 它只是"尽量不用",内存真到绝境照样会用 swap。
# === ★ 比调参数更重要的:从根上解决内存不够 ===
# 这次事故,真正该做的三件事,没有一件是调上面的参数:
# 1. ★ 把 Java 的 -Xmx 调到和物理内存匹配
# (8G 的机器别给 -Xmx6g,留足内核和其他进程的份)
# 2. ★ 加 swap,哪怕只是为了把"猝死"变成"变慢预警"
# 3. ★ 上内存监控告警:available 低于阈值就提前报警,
# 别等 OOM Killer 替你"发现"问题。
# 内核参数是微调;内存的总账,得靠容量和程序本身去平。
修复 6:内存与 OOM 排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 进程凭空消失、应用日志无报错,先查 dmesg ===
$ dmesg -T | grep -i 'out of memory\|killed process'
# 进程被 OOM Killer / 其他外力杀掉,内核日志里有记录。
# === 2. ★ 看内存够不够,看 available,不看 free ===
$ free -h
# buff/cache 高是好事;available 低才是真危险。
# === 3. 揪内存大户用 ps 按 %mem 排序 ===
$ ps aux --sort=-%mem | head
# === 4. ★ 服务器配一块 swap,把"猝死"变成"变慢预警" ===
$ swapon --show # 先看有没有
# 没有就建一块,并写进 /etc/fstab。
# === 5. OOM 杀的是占内存最多的,未必是真凶 ===
# 查到 OOM 别只盯被杀进程,要查整机内存为何耗尽。
# === 6. 进程内存上限要和物理内存匹配 ===
# Java 的 -Xmx、各种缓冲池,别配得超过机器扛得住的量。
# === 7. 排查内存问题的命令链 ===
$ free -h # ① 整机内存,看 available
$ dmesg -T | grep -i oom # ② 有没有发生过 OOM
$ ps aux --sort=-%mem | head # ③ 谁是内存大户
$ cat /proc/meminfo # ④ 内核级内存明细
$ swapon --show # ⑤ 有没有 swap 兜底
$ cat /proc//status | grep -i vm # ⑥ 单个进程内存细节
# 按这个顺序,内存与 OOM 问题基本能定位。
命令速查
需求 命令
=============================================================
看整机内存(看 available) free -h
看内核内存明细 cat /proc/meminfo
按内存占用排序进程 ps aux --sort=-%mem | head
top 里按内存排序 top -o %MEM
确认是否发生过 OOM dmesg -T | grep -i 'out of memory'
从 systemd 日志看 OOM journalctl -k | grep -i oom
看有没有 swap swapon --show
建并启用一块 swap 文件 mkswap /swapfile && swapon /swapfile
看某进程的 OOM 得分 cat /proc//oom_score
保护进程不被 OOM 杀 echo -500 > /proc//oom_score_adj
口诀:进程凭空消失先查 dmesg 找 OOM Killer
内存够不够看 available 不看 free
避坑清单
- 进程凭空消失且应用日志无报错,先 dmesg 查是不是被 OOM Killer 杀的
- 判断内存够不够看 available 那一列,不要被 free 列的小数字吓到
- buff/cache 是内核借用的缓存,内存紧张随时归还,不算"被占用"
- OOM Killer 是内核自保机制,用 SIGKILL,进程没机会留任何日志
- OOM Killer 倾向杀占内存最多的进程,未必是真正泄漏的元凶
- 没有 swap 时内存告急会直接 OOM,有 swap 会先变慢给出预警窗口
- swap 在磁盘上很慢,是缓冲兜底不是内存替代,大量用 swap 等于内存不够
- swap 文件要 chmod 600,并写进 /etc/fstab 才能开机生效
- oom_score_adj 只是换个进程杀,不解决内存总账不平的根本问题
- 进程内存上限(如 Java -Xmx)要和物理内存匹配,留足内核和其他进程
总结
这次"进程半夜凭空消失"的事故,纠正了我一个埋藏得很深的盲区——我排查问题的目光,长期以来只会落在"我的应用"这一个圈子里。在这次之前,我潜意识里有一个从未质疑过的前提:一个进程的死,一定会在【它自己的日志里】留下死因。它要么是抛了未捕获的异常,要么是某段逻辑走进了死胡同,要么是收到了关闭信号体面退出——无论哪种,日志里总该有迹可循。所以当那个 Java 服务半夜消失、而应用日志的结尾却平静得反常——最后几行是再正常不过的业务输出,没有异常、没有报错、没有半个字的"我要走了"——我陷入了彻底的困惑。一个会自己崩溃的进程,日志里会有惨叫;一个收到关闭信号的进程,日志里会有遗言。可眼前这个进程,既没惨叫也没遗言,它就是在说着话的中途,被人从句子中间硬生生掐断了。我对着那截戛然而止的日志想了很久,始终绕不出那个怪圈:凶手不在日志里,可我又坚信凶手一定在日志里。复盘到根上,我才终于意识到,我一直忽略了一个根本的事实——我的应用,从来不是这台机器的主人,它只是内核统治下的一个"租户"。这台机器上真正说了算的,是 Linux 内核。当整台机器的物理内存被耗尽、内核发现自己再不出手整个系统都要卡死时,它会启动一个我此前闻所未闻的自保机制:OOM Killer。OOM Killer 的逻辑冷酷而清晰——它会从所有进程里挑一个"垫背的",一刀杀掉,用它释放出的内存保住整个系统的性命。而它挑选的标准,简单粗暴:谁占的内存最多,谁的嫌疑就最大。我那个被 -Xmx6g 喂得膀大腰圆的 Java 进程,在一台只有 8G 内存的机器上,几乎是 OOM Killer 名单上不容错过的头号目标。更关键的是,OOM Killer 是【内核】亲自动的手,它用的是 SIGKILL——那个不可捕获、不可阻挡的信号。这就解释了那截"干净得反常"的日志:我的进程根本没有收到任何可以反应的信号,它是在毫无预兆的情况下被内核从内存里瞬间抹除的,它当然来不及、也根本没有机会写下任何一个字的遗言。死因不在应用日志里,是因为杀它的根本不是应用层的任何东西——死因写在内核的日志里,写在 dmesg 里,那里白纸黑字躺着一行 Out of memory: Kill process 8123 (java)。这次事故还顺带捅破了我对 Linux 内存的另一个误解。我过去看 free,总是死死盯着 free 那一列,看到它只剩几百兆就心惊肉跳。可实际上,真正该看的是 available 那一列——因为 Linux 会主动把暂时空闲的内存拿去做文件缓存(buff/cache),这部分内存是"借用"的,任何进程一旦真需要,内核立刻归还。free 小不代表危险,available 小才是真的危险。而我这台出事的机器,available 确实已经低到只剩两百多兆,雪上加霜的是它连一块 swap 都没配——没有 swap,内存告急时内核连"把冷数据挪去磁盘撑一会儿"的退路都没有,只能从"内存紧张"这个状态,一步跨进 OOM Killer 这个终局。这次从一截"干净得反常"的日志出发,我最大的收获,是把排查的视野从"我的应用"这个小圈子里挣脱出来,郑重地补上了"内核"这一层。一个进程的命运,从来不只由它自己的代码决定;它脚下那个沉默的内核,握有在资源耗尽时决定"杀谁、留谁"的生杀大权。当一个进程死得没有遗言,不要再徒劳地翻它自己的日志了——它的死讯,记在它那位沉默房东的账本上,记在 dmesg 里。
—— 别看了 · 2026