Linux 遇到 OOM(内存不足)怎么办?新手必看的解决指南
不管你是刚入门还是已经在服务器上摸爬滚打了一段时间,“进程突然消失”这种问题应该都遇到过:上午还跑得好好的 Java 服务,下午 SSH 上去发现进程没了,日志里面什么都没留下;或者数据库在高峰期突然连接不上,重启之后又能坚持几天。去查系统日志,看到一行 Out of memory: Killed process——这就是 OOM(Out Of Memory),换成人话说就是:内存不够用了,内核启动了一个“大扫除”,把你某个进程直接杀掉来腾内存。
这篇文章不打算讲那种让人打瞌睡的理论,而是把我在生产环境里排查 OOM 的经验全部倒出来。从“内核为什么杀我的进程”到“怎么快速定位谁吃了内存”,再到“怎么配置才能尽量不触发 OOM”,全程配指令、配参数、配上坑记录。不管你是刚接触 Linux 的新手,还是已经被 OOM 折磨了几次的运维同学,这篇文章应该都能帮你少走弯路。
1. OOM 是怎么发生的:本质是“内存超卖”的代价与必然
1.1 先理解 Linux 内存分配的逻辑:为什么它敢“过量分配”
很多人对 OOM 的第一个误解是:是不是内存真的被占满了才会触发?答案是:不一定。
Linux 内核默认采用了 Overcommit(超额分配) 策略。什么意思呢?就是一个程序向内核申请内存时,内核不会真的去检查“现在物理内存还有多少空闲”,而是先答应下来,等程序真正写入数据的时候才实际分配物理页。这就像信用卡:银行给你的额度可能比你实际存款高很多,只要你不刷爆,大家相安无事。
这种做法带来一个极大的好处:现代程序(尤其是 fork 出来的子进程、Redis 的 copy-on-write)在申请大块内存但实际使用率不高的时候,系统不至于因为保守而浪费大量物理内存。代价则是:当所有程序同时开始“玩命写内存”,物理内存瞬间被耗尽,内核就得立刻找一个“背锅侠”杀掉,好把空间腾出来。这个背锅侠的挑选过程,就是 OOM Killer(内存耗尽杀手) 的职责。
1.2 内核是如何选出“背锅侠”的
很多新手以为内核会随机挑一个进程杀掉,其实不是。每个进程都有一个 oom_score(分数),分数越高的越容易被杀。这个分数由两部分决定:
- 进程实际使用的内存大小(RSS):用得越多,分数越高。
oom_score_adj(调减值):这是系统管理员可以手动干预的数值,范围从 -1000 到 1000。比如设置成-1000,基本就告诉内核“别杀我”,通常用于关键系统进程。
另外在内核编译以及启动参数中,还有 vm.panic_on_oom、vm.overcommit_memory 这些开关。默认情况下 vm.panic_on_oom=0,表示触发 OOM 时只杀进程、不重启系统;如果设置成 1 或者 2,可能会直接触发 kernel panic 导致整机重启。
这里我想强调一个核心观点:OOM Killer 不是要“惩罚”谁,而是要在最短的时间内释放最多的内存来保住系统。所以它一定优先选那些内存占用大、又看起来“不那么重要”的进程。很遗憾,Java 应用、数据库、大数据计算任务往往就是这样的目标。
1.3 什么时候该慌,什么时候是“良性 OOM”
不是每次出现 OOM 日志都意味着你的服务代码有问题。我总结为三种常见类型:
- 一次性峰值导致突发 OOM:比如双 11 秒杀、活动抽奖,流量瞬间上来,内存短暂见底。这类 OOM 如果发生在可容忍的抖动窗口内,通过扩容或者加缓存就能解决。
- 内存泄漏型 OOM:进程 RSS 持续上升,GC 之后回不来,折腾几天甚至几周后终于被 kill。这种最危险,必须从代码层面排查。
- 系统内存被缓存吃干导致 OOM:Linux 会把空闲内存用作 page cache(文件缓存),这是好事情,但如果你完全没有配置 swap,且缓存不可回收,也会触发 OOM。这种情况偶尔会发生在某些特殊的内核配置或者容器环境中。
拿到一例 OOM 日志,先别急着“调 JVM 参数”或者“加内存条”,你要先回答三个问题:是哪个进程被杀了?它大致吃了多少内存?当时系统总内存和剩余内存是多少?答案基本都在内核日志里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一现场:快速定位“到底是谁 OOM 了”
2.1 从内核日志里捞出关键证据
现在假设你的进程真的被 OOM Kill 了,第一件事不是启动 IDE 改代码,而是去日志里找证据。在绝大多数 Linux 发行版上,内核日志可以通过 dmesg 查看,也可以看 /var/log/messages 或 /var/log/kern.log。实际排查时,我的建议是直接过滤关键词,别拖泥带水:
bash复制# 查看 OOM 相关的内核日志
dmesg -T | grep -Ei 'out of memory|oom-kill|killed process'
# 如果没有 dmesg 权限(比如容器内),尝试看系统日志
journalctl -k --since "24 hours ago" | grep -Ei 'oom|out of memory|killed'
输出类似下面这样:
code复制Out of memory: Killed process 12345 (java) total-vm:8388608kB, anon-rss:3686400kB, file-rss:0kB, shmem-rss:0kB, oom_score_adj:0
这里的关键信息要注意:
Killed process 12345 (java):清楚告诉你被杀的是 PID 12345 的 Java 进程。total-vm:8388608kB:进程总共映射的虚拟内存大小(约 8 GB)。anon-rss:3686400kB:实际物理内存占用量(约 3.6 GB)。这个数字才是判断它是否“真吃多了”的核心参考,因为 total-vm 常常包含大量未被实际使用的虚存页。oom_score_adj:0:该进程没有被手动调整过 OOM 权重,属于“正常候选者”。
如果日志里同时出现大量其他进程的信息(比如 MySQL、Redis、Nginx 也被列出来了),说明整机内存已经极度紧张,不是某一个进程的问题。
2.2 一条命令判断是不是“整机性缺内存”
拿到日志之后,我还建议立刻复核一下当时系统的内存水位。不过如果 OOM 已经发生、进程已经重启,free 看到的往往是“事后”状态,不一定能反映问题峰值。这里推荐两个技巧:
第一,看历史监控数据。如果你用的是 Prometheus + node_exporter,或者阿里云、腾讯云的监控面板,重点拉取 node_memory_MemAvailable_bytes、node_memory_MemFree_bytes 这两个指标在 OOM 时间点前后的曲线。第二,如果没有任何监控,只能靠内存日志中的全局统计:
plaintext复制Node内存状态:
Mem-Info:
MemTotal: 16000 MB
MemFree: 120 MB
MemAvailable: 200 MB
Buffers: 80 MB
Cached: 12000 MB
看到 MemAvailable 极低、Cached 占比极高,那你就要明白了:当时可用内存基本被耗尽,系统的 page cache 也处于峰值。
2.3 排查“没有 OOM 日志但进程死了”的陷阱
这里有一个大家经常踩的坑:应用进程突然消失,但 dmesg 里根本没有 OOM 字样。怎么回事?
可能性有很多,比如:
- 被 supervisor/systemd 重启了,而你在日志里看到的是重启后的时间点。
- 容器平台 OOM:在 Kubernetes 或 Docker 中,即使宿主机内存充足,如果容器超过了自己的 cgroup 内存限制,会被容器运行时或者 cgroup OOM killer 杀掉。宿主机
dmesg不会输出,但容器事件里有Killing container之类的标记。 - 直接被 kill -9 了(有人手动操作或者部署脚本干的)。
排查方法是:先用 journalctl -u 服务名 看服务本身有没有异常退出记录;再看容器的 events;最后才考虑宿主机 OOM。不然你可能盯着一台内存空闲的宿主机,死活找不出原因。
3. 确认是 Java 被 OOM Kill:这类场景的专项排查思路
3.1 为什么 Java 总是“首当其冲”
在服务端场景里,Java 被 OOM Kill 的占比非常高,原因不是你代码写得不好,而是 JVM 本身就是一个“内存吞噬兽”:堆内存、元空间、线程栈、Direct ByteBuffer、JIT 编译器代码缓存,这些加起来很容易占满一整台机器。再加上现代微服务架构下动不动就启动十几个 Java 实例,整机内存压力自然极高。
但注意:JVM 自身也有 OOM 的概念——java.lang.OutOfMemoryError。这和 Linux 系统层面的 OOM Killer 是两回事。前者是 JVM 堆内存不够用了,抛出异常;后者是操作系统把 JVM 进程杀了,应用层面根本来不及反应。很多新手把这两者混在一起,结果调了一堆 -Xmx,却忽略了系统层面的东西。
3.2 从退出码和日志快速判断
一个简单判断方法:如果 Java 进程是被 Linux OOM Killer 杀掉的,进程退出码通常是 137(128 + 9,SIGKILL)。如果进程退出码是 1 或者 3,并且输出里带有 java.lang.OutOfMemoryError,那大概率是 JVM 自身的堆内存溢出。
另外,JVM 被系统 kill 前不会打印任何异常堆栈,应用日志最后一条往往是“一条业务日志之后戛然而止”。如果你发现自己的应用日志在崩溃前一点报错都没有,同时内核日志里有 OOM,那就是系统杀的,不是 JVM 自己吐出来的。
3.3 优化 JVM 配置来降低被杀概率
这里不打算罗列全部 JVM 参数,只讲和 OOM 相关的几个最关键的实践。
第一,合理设置堆内存。经验公式是:容器/整机可用内存的 60%~75%。如果你是一台 8 GB 的小机器,-Xmx 设置到 6 GB 就很危险,因为除了堆,还有 Metaspace、线程栈、Direct Memory,这些加起来可能上 GB。我通常会给 8 GB 的机器留 2~2.5 GB 给系统和非堆内存。
第二,添加 -XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath=/data/logs/。这样即使 JVM 自身 OOM,也会留下堆转储文件,后面的分析就有依据。
第三,如果你使用容器化部署,务必用 JVM 能感知容器内存上限的版本和参数。JDK 8u131+ 支持 -XX:+UseCGroupMemoryLimitForHeap(但不推荐),JDK 8u191+ 以及更高版本默认可以从容器的 cgroup 限制自动计算堆大小。如果版本太老,JVM 看到的可能是宿主机的内存总量,自动算出来的堆大小就可能超过容器限制,从而触发容器级 OOM。
还有一点很多人不知道:如果进程为辅 Java 进程,比如 Kafka、Elasticsearch、Flink,它们底层都是 JVM,但配置入口和优化偏好差别很大。ES 的 jvm.options 默认只给了 4 GB,在没有明确调整的情况下,很容易在节点内存配置较大时出现浪费,或者反过来因为堆大小超过物理内存的一半而触发各种问题。
4. 如何“保命”:从配置、监控到应急预案
4.1 swap 到底要不要开,以及怎么开
关于 swap 这个话题争论很多。一部分人说“有 swap 就能避免 OOM”,一部分人说“swap 有什么用?速度太慢,不如不开”。我的个人建议是:服务器上最好有一点点 swap,用于应急,但不要指望它解决正常业务的内存需求。
开启 swap 的意义在于:当内存出现瞬时高峰,比如编译大型工程、启动某些重型应用时,内核有机会把一些不常用的内存页换到磁盘上,给紧急任务腾出空间。没有 swap,内存瞬时耗尽时系统就会直接进入 OOM killer 流程,几乎没有缓冲时间。
在 Linux 查看当前 swap 状态:
bash复制free -h
## 如果 swap 为 0,可以快速创建临时 swapfile
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
为了让 swap 只在真正紧急的时候才被使用,可以把 vm.swappiness 调低一些,比如 10。这个值表示内核倾向于使用 swap 的程度,0 表示尽量不用,100 表示积极使用。设置方式:
bash复制sudo sysctl vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
不过我要说清楚:swap 不是万能药。如果一台机器的内存确实长期不够,加 swap 只是一位临时工,该加内存条还是要加,该缩减进程规模还是要缩减,否则 swap 会先耗尽,然后照样触发 OOM。
4.2 用 vm.overcommit 参数做“安全阀”
如果你处于一个比较极端的场景:单机运行了多个业务,又不方便立刻上容器或 cgroup 限制,可以通过调整 vm.overcommit_memory 来控制内核的“乐观程度”。
默认值 0 表示内核自己做启发式判断,大多数情况下允许明显的超卖。设置成 2 表示禁止过量分配,内核按“可能的最大内存需求”来拒绝部分申请。这个模式比较保守,可能让某些正常的大内存申请失败,但对于频繁 OOM 的机器,有时候反而是保命的关键。
注意:调整这个参数要非常慎重,它不是针对单个进程,而是全局策略。我曾经在测试环境把它设成 2 后,某个应用直接报 Cannot allocate memory,因为它申请的大块虚拟内存超过了限制。
4.3 用 systemd/cgroup 限制单个服务的内存上限
在现代化部署中,我最推荐的方式是 给关键服务设置 cgroup 内存限制。这样内核会在某些进程达到限制而不是整机耗尽时,先把“最过分的那个进程”杀掉,不至于影响其他服务。
如果你使用 systemd 启动服务,可以在 service 文件里加:
ini复制[Service]
MemoryMax=4G
MemoryHigh=3G
MemoryHigh 是一个软限制,当内存占用超过该值,内核会加大对进程的内存回收力度;MemoryMax 是硬限制,超过后进程可能被 OOM kill 或者直接被 SIGKILL。
如果你用的是 Docker,对应参数就是 --memory=4g 或 Docker Compose 里的 mem_limit。Kubernetes 则用 limits.memory 来配置。
这样做的一个额外好处是:当某个容器内存超标被 kill,宿主机的监控和日志可以清楚看到“这个容器超过了自己申请的内存”而不是“整机内存不足”,排查范围一下子缩小了。
4.4 监控与预警:在问题出现之前拦住它
OOM 的可怕之处在于它往往是“偶发”的,今天我们看完这篇攻略,重启服务后可能几天都不再出现,然后它在一个深夜又来一次。所以监控才是治本方案之一。
建议至少监控以下指标:
- 可用内存
MemAvailable的绝对值与百分比(低于 10% 或绝对值低于某个阈值就要告警); - 内存使用增长速率(判断有没有泄漏);
- 每个进程的 RSS 占用(用
ps aux --sort=-%mem可以随时看); - 内核日志中
oom_kill事件计数(可以用 node_exporter 的 textfile collector 或者自定义脚本)。
写一个简单的巡检脚本可以这么考虑:
bash复制#!/bin/bash
OOM_LOG_COUNT=$(dmesg -T | grep -c "Out of memory" --since="10 minutes ago" 2>/dev/null || dmesg | grep -c "Out of memory")
if [ "$OOM_LOG_COUNT" -gt "0" ]; then
echo "检测到 OOM 事件!" | mail -s "OOM Alert" your@email.com
fi
在生产环境,更好的方案是接入 Prometheus + Alertmanager,不过对于只有几台机器的新手来说,先用一个简单的 cron 脚本看住日志也行,总比两眼一抹黑强。
5. 错误示范与避坑技巧:这些“经验”可能是错的
5.1 误区一:调大 -Xmx 就能解决所有问题
“我 Java 程序被杀了,赶紧把 -Xmx 调大。”这是最典型的错误操作。如果系统的内存总量不变,-Xmx 调大意味着 JVM 会更快吃满可用内存,反而让你更容易被杀。调大堆内存的前提是:你已经确认系统有足够的空闲内存,且有其他参数(非堆内存、操作系统缓存)在占用。
5.2 误区二:直接把 OOM 进程设置成 oom_score_adj=-1000
有些同学听说调这个值可以防止被 kill,于是给数据库或核心服务设置了 -1000。这非常危险。假设一台机器上只有两个大内存进程,你把一个保护起来,另一个被杀了还不够,重启后仍然内存不足,这时候内核可能直接选择 panic,整机挂掉,比你一个服务挂掉还严重。
正确做法是:对关键服务,不设置 -1000,而是给它所在的 cgroup 设置独立内存配额,从源头上确保它不会占满整机内存。设置调度优先级和 OOM 逃逸只能作为临时手段,长期来看必须把资源边界划清楚。
5.3 误区三:只查进程,不查“匿名页”和“页缓存”
内核 OOM 时通常会打印很长一段内存统计,里面能看到 AnonPages、FilePages、Shmem 等字段。新手往往只盯着自己的进程看,结果忽略了 Shmem 占用巨大,可能是某个临时目录 /dev/shm 里写了大文件,或者 tmpfs 被过度使用。
遇到这种场景,不需要改你的应用代码,而是要去排查是否有程序把大量数据写进了 /dev/shm,或者关键词搜一下 tmpfs 的使用情况:
bash复制df -h /dev/shm
find /run -type f -size +100M 2>/dev/null
6. 站在资深运维的角度:OOM 后续的真正复盘
6.1 记录现场,而不要急着重启
方法听起来很反直觉,但我要强调:进程被 OOM Kill 之后,第一选择不是立刻重启,而是先留出现场证据。如果这个进程是一个状态比较多的 Java 服务,重启后 JVM 堆会被清空,许多排查线索(比如内存飙高时的引用链、GC 后的内存曲线)都会丢失。
如果你能给关键 Java 服务提前加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heapdump/,那进程即使在 JVM 层面发生了 OOM,我们也有 dump 文件可用,但如果是操作系统层面被 kill,这个参数不会生效。这种情况下,我们能依靠的是:
- 监控工具记录到的内存占用曲线
- JVM 的 GC 日志(如果你开了
-Xlog:gc) - 系统日志里 OOM 发生前的内存统计
如果内存泄漏的迹象比较明显(RSS 只涨不降),下一步可以用 pmap -x pid 查看进程详细的内存映射,或者用 jcmd、jmap 对 Java 进程做实时 dump。但说实话,这些操作最好在内存还在高位时就做,等 kill 之后再做已经晚了。
所以对于正式环境,我强烈建议:给所有重要的 JVM 都用上 GC 日志和健康检查脚本,不要等出问题了再研究自己该看什么。
6.2 分析根因时的“两条腿走路”
复盘 OOM 要从两个维度同时进行:
- 第一个维度,容量规划:这台机器配置是否满足业务正常峰值?如果峰值需求就是比物理内存大,那 OOM 就只是表象,根因是规划不足。解决方式是扩容,或者在架构层面做内存限制和降级。
- 第二个维度,应用代码/参数:进程是否泄漏?缓存是否无限增长?线程池是否无界?逐个检查,才能根治。
举个我实际处理过的案例:一个 Go 写的服务,平时占内存很低,某天开始 OOM 频繁,但内存托管指标看起来并不高。后来我们开会时对照业务流量和时间节点,才发现是有一个批处理任务进来后,在短时间内创建了大量临时 goroutine 和 map 结构。扩容之后问题消失,但本质上还是需要在代码里加一个流量控制和内存上限保护,防止极端流量再次打爆。
6.3 从“被动救火”转向“主动预案”
这几年我和团队在服务器稳定性上投入最多的,不是出了一个事故就修复一个事故,而是把类问题预案化、自动化。比如我们规定每台服务器都配置好 systemd 的资源限制,所有 Java 服务上线时必须带上 HeapDump 参数,所有容器设置 limits 和 requests。这套“护栏”铺好之后,后续再遇到内存问题,至少排查时间能缩短一半。
如果你只有一两台服务器,也可以用脚本做一个“自我保护”:定期检测内存水位,超过阈值就把非核心进程重启或者停止。这种做法虽然粗暴,但确实能防止整机因为一个进程泄漏而彻底瘫掉。
写在最后
OOM 本身不可怕,怕的是毫无头绪地瞎试。整个排查思路可以总结为一句话:先判断是不是系统级内存耗尽,再判断是哪个进程惹的祸,最后看是容量问题还是代码问题。实际操作中,先把 dmesg 里的日志捞出来,再看监控曲线,再决定是调参、扩容还是改代码,这个顺序基本不会错。
最后分享两个小技巧:第一,在 /etc/sysctl.conf 里把 vm.min_free_kbytes 适当调高(比如物理内存的 0.5%~1%),这样内核会提前为紧急分配保留一部分内存,避免内存耗尽时连野马都拉不住;第二,给服务器的 root 用户配置一个接收内核告警邮件的别名,因为很多 OOM 事件在监控系统还没报警时,内核日志里已经写得明明白白了。提前做好这些准备,真正遇到问题的时候,你才能从容地打开终端而不是手忙脚乱地到处查命令。
