
1. 问题现象free命令看着内存充足程序怎么突然没了这是一个非常典型的Linux VPS运维问题也是很多新手甚至有点经验的朋友都会踩的坑。你用free -h一看内存明明还剩一半Swap也没用多少但跑到一半的Java程序、数据库进程、Node服务说没就没了日志里连个报错都找不到感觉像是被系统“暗杀”了。其实这就是Linux内核的OOM KillerOut Of Memory Killer内存溢出杀手干的活。问题是为什么它要杀掉一个看起来“内存够用”的程序这里面的逻辑和你平时在Windows上看内存占用是完全两回事。今天我把这个问题掰开揉碎讲清楚从内核机制到排查方法再到最终解决方案一次说透。2. 核心原理Linux的内存远比你想象中复杂2.1 free命令显示的“剩余内存”是假象很多兄弟看内存够不够习惯性看free -h里的free那一列也就是剩余内存。这个数字在Linux里几乎毫无意义。Linux的设计哲学就是“内存闲着就是浪费”它会用尽一切办法把空闲内存拿去用比如给磁盘文件做page cache页缓存给进程做文件映射。你看到的free列很小其实是因为Linux把大部分内存都拿去当缓存了而缓存是可回收的。真正需要关注的是available这一列这个才是“在不触发swap的情况下还能分配给新程序的内存估算值”。但即便是available很充足程序还是可能被杀掉这就是问题的诡异之处也是我们这篇文章要重点深入的地方。举个例子我有一台2G内存的VPS跑着一个Java服务和一个MySQL。free -h显示total2Gused1.2Gfree500Mshared1Mbuff/cache300Mavailable700M看起来挺健康的对吧但Java服务还是经常被杀掉。后来查dmesg日志才发现罪魁祸首根本不是物理内存不够而是OOM Killer在误杀。为什么这就需要理解Linux内存分配的另一个关键机制overcommit。2.2 Linux的overcommit机制允许“超额预订”Linux内核默认采用的是启发式overcommit策略也就是允许进程申请超过物理内存总量的虚拟内存。为什么因为大多数程序申请内存时是按“最大可能的峰值”去申请的但实际用到的往往远没有那么高。这就像你办了一张额度10万的信用卡但每个月实际只刷1万银行不会因为你额度是10万就觉得你欠了10万。内核这种策略的好处是内存利用率大幅提高坏处是当多个程序同时把虚拟内存变成实际使用也就是触碰到那些申请的页面的时候物理内存就会瞬间爆掉。这个时候内核必须启动OOM Killer来杀进程释放内存否则整个系统会hang死。很多人第一次遇到这种情况时会想我明明还有500M free为什么内核要杀人原因就是那个瞬间可能有某个程序申请了几百M内存加上已有的占用已经超过了commit限制默认情况下是物理内存swap的一定比例而free列那点余量根本扛不住这种突发需求。2.3 page cache、共享内存与swap的三重奏再说一个特别容易被忽略的点内核在计算“是否需要杀进程”的时候算法是相当复杂的。它不仅要看当前物理内存的剩余情况还要看page cache的可回收性、swap的使用率、每个进程的oom_score等。page cache也就是文件缓存是内核认为“最愿意牺牲”的内存。当一个进程突然需要大量内存时内核会先尝试回收page cache这个回收动作是很快的。但如果page cache已经被回收得差不多了又或者回收的速度跟不上进程申请的速度内核就会立刻进入OOM判定流程。还有一个坑是共享内存和tmpfs。比如PHP-FPM用到的/dev/shm或者你手动挂载的tmpfs分区它们占用的内存会计入Shmem但通过free命令看used的时候这部分经常被算到buff/cache里导致你误以为还有大量可回收缓存。实际上tmpfs是不可回收的一旦写进去除非你删文件否则它一直占着物理内存。这就解释了为什么有时候你看到buffer/cache还有好几百M程序却依然被OOM杀掉——因为那部分可能压根收不回来。3. 真正的元凶OOM Killer的判定逻辑与触发场景3.1 oom_score内核如何挑选“牺牲品”在讲判定逻辑前先看一个关键文件/proc/pid/oom_score。这个分数是内核为每个进程打的分分数越高在内存不足时越容易被杀掉。默认情况下它综合考虑了进程的驻留内存大小RSS、页表大小、swap使用量、进程运行时间、进程优先级等因素。具体的计算逻辑类似基础分进程占用的物理内存RSS swap折算成页数再乘以一个权重系数。调整分运行时间越长分越低内核认为你运行了这么久杀掉你更可惜有root权限的进程分相对低一点子进程越多分越高因为杀掉它可以连带释放一堆子进程的内存。加分可以手动调整。你可以在/proc/pid/oom_score_adj里写一个数值范围是-1000到1000正数加分数容易被杀负数减分数抗杀-1000就是完全禁用OOM Killer杀它。所以在同样的内存压力下一个占内存大、运行时间短、没有权限调整的普通进程很容易成为替罪羊。我记得有一次在VPS上跑一个Python爬虫它就是个“内存胖子”每次OOM都是先杀它根本不是因为它不重要只是因为它内存分最高。3.2 常见的触发场景不是只有“内存真不够”才会被杀我总结了几个实际环境中容易被OOM误杀的经典场景这些场景的共同特点就是“表面看内存充足实际已经到了悬崖边”。第一瞬时内存峰值。Java程序的堆内存Heap配置不当或者Python脚本里加载了一整个大文件瞬间申请了一两百M内存。内核这时候如果发现物理内存不足且page cache回收不过来立刻触发OOM。你的程序在那一瞬间申请内存失败然后被杀等你看日志的时候内存已经被释放了所以你会觉得莫名其妙。第二memory cgroup的限制。现在的VPS大多用了KVM虚拟化宿主机上每个虚拟机都有自己的cgroup限制。更重要的是你如果自己装了Docker或者用了systemd管理服务每个服务可能都有MemoryLimit限制。进程实际用的物理内存没超过总内存但超过了cgroup的限制照样会被杀。我遇到过一个案例一个Node服务在容器里设置了memory: 512M但服务本身峰值需要600M于是容器内的进程被反复杀掉但VPS整体的内存完全够用。第三swap配置不合理。很多VPS默认没开swap或者swap很小。当一个进程占用的匿名内存anonymous memory超过物理内存内核需要把不常用的页面换到swap里。如果swap太小内存压力就无法通过swap缓解OOM会更快触发。反过来如果swap太大且是SSD上的swap分区系统可能会变得极慢因为内核在疯狂换页这时候OOM Killer也会介入杀掉一些进程来“止损”。第四内存碎片化。这个比较高级但也真实存在。物理内存虽然总量有剩余但被分割成了很多小块没有连续的大块内存可供内核或程序做高order分配比如需要连续的2MB、4MB页面做页表。这种情况在长时间运行的VPS上偶尔会出现虽然不如前面几种常见。3.3 dmesg日志破解“无头悬案”的唯一线索遇到程序突然被杀第一件事不是去改代码而是去看内核日志。这是排查OOM问题的黄金钥匙90%的“无头悬案”都能在这里找到答案。dmesg | grep -i killed process # 或者查看完整OOM信息 dmesg | grep -i -A 10 out of memory # 如果dmesg被权限限制可以看journalctl journalctl -k -f journalctl -k | grep -i oom日志里你会看到类似这样的信息Out of memory: Killed process 12345 (java) total-vm:5898240kB, anon-rss:1536000kB, file-rss:0kB, shmem-rss:0kB这一行告诉你哪个进程被杀了、它的虚拟内存total-vm有多少、实际占用物理内存的匿名部分anon-rss有多少。如果进程被杀时的物理内存占用看起来并不高那就要留意是不是cgroup的限制导致的。同时日志上方通常还有一段内存信息显示各个zone的使用情况Node 0 DMA32 free:12340kB min:12000kB low:14800kB high:17600kB这里需要关注free和min的关系。如果free接近于min说明内核已经处于内存压力非常大的状态触发OOM是在预料之内的——即使你整体看还有几百兆available但在某个内存zone里已经没多少可用的连续页面了。4. 一整套排查手段从直觉到证据链4.1 基础信息采集free、ps、top的正确打开方式排查OOM问题不能只靠事后看日志。正确的做法是建立一个长期可追溯的监控体系。先说几个基础命令的正确用法。free -h要重点看available列不要看free列。同时注意buff/cache的数值如果它很大比如超过总内存的50%说明系统里有很多文件数据在缓存里这本身不是坏事。但如果伴随频繁的OOM就要计算一下available swap 在很短时间内被消耗殆尽这说明有“内存黑洞”进程在快速吃内存。top和htop看的是动态数据。按下M键可以按内存占用排序。但你要区分几个概念VSZ虚拟内存大小进程申请的虚拟地址空间包含没实际用到的部分。RSS驻留内存大小进程当前实际占用的物理内存。PSS比例集大小考虑共享库后按进程数平均分摊这才是相对真实的每进程占用。如果你的程序是Python或Ruby这类解释型语言它的RSS相对较低如果是JavaRSS会明显偏高因为JVM会把Heap和元空间都算进去而且GC后内存也不一定立刻还给操作系统。我自己的习惯是写一个简单的cron任务每隔1分钟把free -h、ps aux --sort-%mem | head -20、以及每个关键进程的oom_score和oom_score_adj记录下来。这样即使程序被杀了我也能还原出杀它之前5分钟的内存走势。4.2 深入cgroup看看是不是“局部分配不均”如果你用了Docker、LXC这类容器或者systemd管理的关键服务一定要检查cgroup的内存限制。这里有个常见的认知误区你以为程序是跑在VPS里实际上它跑在VPS里的容器里而容器有独立的内存限制。查看Docker容器的内存限制docker inspect container_id | grep -i memory查看systemd服务的限制systemctl show service_name | grep -i memory运行中的cgroup限额也可以在/sys/fs/cgroup/memory/下直接看cat /sys/fs/cgroup/memory/memory.limit_in_bytes cat /sys/fs/cgroup/memory/memory.usage_in_bytes如果你看到usage已经接近limit而宿主机整体内存还很充足那问题就定位清楚了不是VPS物理内存不够是cgroup这个“小笼子”关太紧了。这时候要么调大限制要么优化进程自身的内存占用二选一。4.3 观察swap和page cache的状态手把手教你判断内存压力除了freevmstat是这个场景下一个不可多得的利器vmstat 2 10重点关注几个列r运行队列长度如果持续大于CPU核数说明系统在超负荷运转。si、so从swap换入、换出的数据量块/秒如果持续非零说明内存压力很大系统正在频繁交换页面。这种状态下程序偶发被杀就一点也不奇怪。cs上下文切换次数太高说明内核在频繁调度也可能是内存压力的连带效应。还有一个文件值得关注/proc/pressure/memory。这个文件记录的是内存压力信息是内核PSIPressure Stall Information机制的一部分。你可以这样读取cat /proc/pressure/memory some avg100.00 avg600.00 avg3000.00 total123456789 full avg100.00 avg600.00 avg3000.00 total0其中some列表示有任意进程因内存不足而等待的时间比例full表示所有进程都在等待。如果avg10经常超过1甚至5说明内存已经处于持续高压状态OOM只是时间问题。5. 一线实操解决OOM问题的6个真实方案5.1 调整oom_score_adj给“重要进程”穿上防弹衣如果你明确知道哪个进程绝对不能被杀比如数据库、主业务进程可以直接调整它的oom_score_adj。这个方法简单粗暴但极其有效。# 找到目标进程的PID pgrep -f java|mysql # 查看当前分数 cat /proc/$(pidof mysqld)/oom_score # 调整分数降低被杀概率 echo -500 /proc/$(pidof mysqld)/oom_score_adjoom_score_adj的有效范围是-1000到1000。设为-1000时OOM Killer不会选择这个进程除非该进程是系统唯一选择那也不保证一定不被杀但基本也是最后的最后。设为正值则增加被选中的概率。设置完后可以用cat /proc/$(pidof mysqld)/oom_score确认调整后的实际分数。需要注意的是这个调整在进程重启后会失效如果你想长期生效可以在systemd服务文件里配置[Service] OOMScoreAdjust-800如果是Docker可以在docker-compose.yml里写services: mysql: oom_score_adj: -8005.2 调整overcommit策略从源头限制“超额预订”如果你反复遇到OOM并且确认是某个进程一次性申请了太多虚拟内存导致的可以考虑调整内核的overcommit策略。有三个档位0启发式策略默认值。内核根据可用内存情况允许合理范围内的超额申请。1总是overcommit。不管进程申请多少内核都说“行”反正分配内存发生在实际触碰页面时。这个模式下系统崩溃风险高不建议用于生产。2禁止overcommit。内核会严格计算所有进程申请的虚拟内存总量不允许超过物理内存swap的某个比例。这个比例由overcommit_ratio控制默认是50也就是物理内存swap的50%。修改方式sysctl vm.overcommit_memory2 sysctl vm.overcommit_ratio80 # 永久生效 echo vm.overcommit_memory2 /etc/sysctl.conf echo vm.overcommit_ratio80 /etc/sysctl.conf sysctl -p把overcommit_memory设为2等于提前给每个进程的内存申请上了一道锁申请的时候如果超过限额malloc或mmap直接返回失败而不是等真用的时候才爆。代价是有些程序的虚拟内存申请模式比较激进比如JVM的默认Heap预分配、某些数据库的buffer pool它们会被误伤启动就报错。所以这个方案需要对业务足够了解并且做好压力测试后再上。5.3 合理配置swap用磁盘空间换取进程生存很多VPS厂商默认只给你几百M甚至不给swap。这在高内存压力的场景下非常危险。swap相当于内存的“备胎”虽然速度慢但关键时刻能救命。给VPS加swap的办法如下# 创建一个2G大小的swap文件 fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile # 开机自动挂载 echo /swapfile none swap sw 0 0 /etc/fstab注意几个细节fallocate不要用于这个场景有些文件系统会生成带洞的文件导致swapon时出错更稳的做法是用dd if/dev/zero of/swapfile bs1M count2048。swap不是越大越好。如果swap太大系统会倾向于把不常用的进程内存换出去导致明明物理内存够用程序却因为换页变慢到像死机一样。我个人经验VPS内存2G以内的swap给2G够用4G以上swap给4G足矣。swap的优先级swappiness也要调一下。vm.swappiness10是比较合理的值表示只在内存压力明显时才使用swap避免系统频繁换页。5.4 优化系统级参数给内核留足“呼吸空间”有两个sysctl参数经常被大家忽略但对OOM有直接缓解作用。第一个是vm.min_free_kbytes它控制内核为每个zone保留的最低空闲内存用于应对紧急情况比如突发内存申请。默认值通常很低对于2G内存的VPS大概只有几十M。调高这个值可以尽量避免内存瞬间耗尽代价是可用内存会少一些。我一般会调到物理内存的1%~2%# 以2G内存为例 sysctl vm.min_free_kbytes32768第二个是vm.vfs_cache_pressure它控制内核回收inode和dentry缓存也就是文件系统元数据缓存的倾向。默认值是100数值越高回收越激进。如果系统经常出现文件操作频繁导致缓存暴涨可以适当调低sysctl vm.vfs_cache_pressure50这个参数不能解决OOM本身但可以减少“page cache回收不及时导致内存紧张”的误判概率。5.5 从程序侧出发直击内存占用的根源系统层面的调整都是“治标”真正一劳永逸的办法还是让程序内存使用更克制。这里分享几个我在不同语言环境下的实战经验。对Java来说JVM默认的堆大小是物理内存的1/4这导致在2G内存的VPS上JVM一启动就占掉500M。如果你跑的是个小服务务必显式设置-Xms256m -Xmx512m同时根据GC日志调整Metaspace大小。还要注意JVM会为线程栈预留内存每个线程默认1M线程池开太大也会造成物理内存暴涨——这部分不受-Xmx控制。对Python来说最常见的是pandas读取大CSV文件时一次性把整个文件load进内存问题就出在“一次性”这三个字上。正确的做法是用迭代器分块读取比如pd.read_csv(..., chunksize10000)然后逐块处理。还有一些内存驻留的大对象处理完后要用del显式删除再调用gc.collect()。对Node.js来说V8引擎默认的老生代堆上限大约是1.4G~2G但这不是全部。如果你用了大量Buffer对象比如做文件上传、图片缩放这些Buffer是分配在堆外的不受V8堆限制控制很容易成为“内存无底洞”。会用--max-old-space-size调整堆大小更要会用Buffer.poolSize和流式处理来防止Buffer无限堆积。5.6 快速缓解紧急情况下如何“抢救”被杀的程序如果程序被杀了而你现在又没法立刻优化代码可以用一个简单的守护脚本兜底。比如用systemd的自动重启机制[Service] Restarton-failure RestartSec5 StartLimitIntervalSec0如果是Docker加--restartalways。这样即使OOM杀了进程系统也能在5秒内拉起来业务中断时间非常短。但要注意这个只能是应急方案不能成为常态。如果程序总是被杀重启业务数据的一致性风险会非常高尤其对数据库类服务——OOM强杀可不等于优雅停机事务可能只有一半落盘。6. 模拟实验亲手制造一次OOM彻底看清全过程理论说再多不如亲手操作一次。我建议你在测试VPS上做个“安全”的模拟亲眼看看程序是怎么被杀的这样以后遇到真问题心里就有底了。6.1 制造OOM现场写一个极简易的内存吃满脚本故意在一个循环里不停申请数组# eat_memory.py import time holder [] while True: # 每次申请大约10MB内存 holder.append(bytearray(1024 * 1024 * 10)) time.sleep(0.05)在另一个终端用free -h实时观察内存变化。随着数组不断append可用内存迅速下降当接近耗尽时你会看到进程被杀终端输出类似Killed或者进程挂在那里但已经停止工作。此时立刻去查看日志dmesg | tail -20 journalctl -k --since 1 minute ago你应该能看到完整的OOM报告包括每个进程的内存score以及最终选择了哪个进程作为“牺牲品”。6.2 查看完整OOM Report一份典型的OOM日志长这样Out of memory: Killed process 2543 (python3) total-vm:2097152kB, anon-rss:1567800kB, file-rss:0kB, shmem-rss:0kB oom_reaper: reaped process 2543 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB在Out of memory前面还有一大段内存状态描述Mem-Info: active_anon:262144 inactive_anon:8192 isolated_anon:0 active_file:4096 inactive_file:10240 isolated_file:0 unevictable:0 dirty:0 writeback:0 slab_reclaimable:4096 slab_unreclaimable:8192 mapped:10240 shmem:0 pagetables:2048新手看这段可能会懵其实核心信息就是active_anon也就是活跃的匿名内存。anon内存是进程实际使用且无法通过回收page cache获得的内存它一旦暴涨内核就必须立马处理。我实验里看到的active_anon从几百M飙到1.5G然后OOM触发全程不超过10秒。6.3 实验后的参数验证接着你在同样环境下调低oom_score_adj再次实验会发现Python脚本被杀之前内核优先找了其他分数更高的进程。多次实验后你对OOM Killer的行为模式就会有一个很直观的感知它不像Windows那样弹个窗问你“是否关闭程序”而是安静利落地挑一个它认为最“划算”的进程直接杀掉。这个实验的最大价值就是让你理解一个道理对内核来说保住整个系统不卡死不崩溃比保住你某个“重要进程”更重要。所以做运维/开发必须主动告诉内核“哪个进程更重要”否则“公平”就意味着“随机”。7. 常见问题速查与避坑指南7.1 故障排查问题对照表我把平时的排查经验整理成了一张速查表遇到OOM问题直接按图索骥现象大概率原因快速定位方法解决方向free显示内存充足程序被杀dmesg有OOM记录overcommit触发不可避免的OOMdmesggrep -i oom 看anon-rss和total-vm日志显示“memory cgroup limit exceeded”容器/cgroup限额过小cat /sys/fs/cgroup/memory/memory.limit_in_bytes调大limit / docker compose改memory参数dmesg里没有OOM程序就消失了被cgroup杀、被supervisor正常杀掉、或进程自身崩溃systemctl status service、journalctl -u service看服务管理器日志Python/Node程序启动后无征兆退出内存分配失败有些语言会直接abort程序stderr里可能报“Cannot allocate memory”检查malloc调用 / 调整overcommit策略为1试试谨慎程序被杀后系统变得极慢swap在疯狂换页vmstat里si/so持续非零free -h里si和so列很大增加物理内存或优化程序内存占用7.2 我自己踩过的几个坑第一个坑过度依赖dmesg。有些VPS的虚拟化环境下dmesg里可能看不到完整OOM日志因为它被宿主机的日志系统吞掉了。这时候要换思路用journalctl -k如果还没有就看看/var/log/kern.log或者/var/log/messages。再找不到就得靠监控体系还原现场了。第二个坑对JVM的“物理内存”认识不足。JVM的RSS不等于-Xmx设置的值。除了堆外还有Metaspace、线程栈、即时编译器JIT缓存、直接内存Direct Memory。我在2G内存的VPS上跑一个-Xmx512m的Spring Boot服务结果RSS稳定在1.2G就是因为创建了太多线程并且直接用ByteBuffer做了大量I/O。排查这种事别光看-Xmx得用jcmd pid VM.native_memory summary看看JVM内部各个区域到底用了多少。第三个坑忘了检查mlock。有些程序比如Redis的--mlockall或者Elasticsearch的bootstrap.mlockall会把内存锁在物理内存里不允许内核回收。这会显著增加OOM Killer的误杀概率。如果你开了mlock那么有锁的系统内存就不是一个“可牺牲”的page cache了内核只能杀其他进程。排查时注意看是否有程序在/proc/pid/status里的Mems_allowed_list异常以及VmLck的值。7.3 避坑建议监控比排查更重要说实话OOM问题最令人头疼的不是解决而是“事后才发现”。程序被杀了你已经错失了最佳定位时机剩下的都是回忆和猜测。所以我的最终建议是建立一个基础的监控体系。不用上什么高大上的PrometheusGrafana对单台VPS来说一个30行以内的Shell脚本就够#!/bin/bash # memory_monitor.sh while true; do memory_data$(free -h) top_mem$(ps aux --sort-%mem | head -10) echo $(date %Y-%m-%d %H:%M:%S) /var/log/mem_monitor.log echo $memory_data /var/log/mem_monitor.log echo $top_mem /var/log/mem_monitor.log sleep 60 done这个脚本成本极低但在程序被杀后你能立刻翻到杀它前内存的使用情况、Top内存进程名单再结合dmesg日志基本90%的问题都能快速定位。我在生产环境用了两年救过我好几次。8. 最后再分享一个实战小技巧我在排查了无数台VPS的OOM问题后有一个非常个人化的习惯拿到一台新VPS第一件事就是改三个参数。vm.swappiness10、vm.min_free_kbytes设置为物理内存的1%~2%、vm.overcommit_memory0保持默认除非业务明确要求。同时给所有关键服务写进systemd单元文件明确OOMScoreAdjust-500以上。这套组合拳打完服务器在常规负载下基本不会再遇到玄学杀进程。剩下的事情就是监控、监控、再监控。程序被杀不可怕可怕的是你连它为什么被杀都不知道。