
1. 从一次深夜报警说起OOM 到底是什么前几天凌晨我负责的一个数据采集服务突然挂了。登录服务器一看dmesg -T的输出里躺着一行熟悉又扎眼的日志Out of memory: Killed process 4521 (java) total-vm:..., anon-rss:...。业务方在群里发消息问“服务怎么死了”我一边安抚情绪一边在脑子里过了三遍排查思路。这已经不是第一次遇到 OOM但每次遇到都有一些新体会。OOM 全称 Out Of Memory指 Linux 内核在系统物理内存严重不足时触发的一套“紧急自救”机制。别把它想得太玄打个比方你住一个整租的屋子房东规定所有房间加起来不能超过总面积结果你把每一间都塞满了东西连过道都占了这时候房东就要从你的房间里挑一件最佔地方的东西扔出去——那个“被扔东西”的动作就是内核的 OOM Killer 在杀进程。对新手来说第一次遇到 OOM 的体验一般是服务莫名其妙退出了free -h一看内存还有几个 G可日志里明明写着 Out of memory。这个“内存看着够却还是被杀”的困惑恰恰是理解 OOM 的关键入口。系统剩余内存不够给内核自己用了或者一次突发的连续内存申请导致了内存水位瞬间告急OOM Killer 就会直接介入杀掉它认为“最該死”的进程来释放空间。这篇文章不打算只丢给你一句“加内存就完事”。我会从现象识别、内核机制、排查手段、长期策略四个层面把 OOM 这件“小事”彻底摊开完整覆盖我这些年在一线踩过的坑和验证过的方案尽量让新手看完之后下次再遇到 OOM 能心里有数、手上有招。2. OOM 的触发链条与内核决策逻辑2.1 内存到底是怎么“满”的“内存不足”不是一个瞬间事件而是一条由慢到快的滑坡。程序申请内存时内核并不会立刻分配物理页而是走“请求-延迟分配”机制申请的时候只记账真正写数据的时候才分配物理页。这就导致了一种很常见的假象——一堆进程各自申请了一大片虚拟内存系统看起来还剩不少空闲但某个瞬间所有进程同时写数据物理页瞬间被瓜分干净。这种“同时写”的场景在服务端很常见。比如一个 Java 应用在做 Full GC 前需要快速分配一个大数组又比如一堆临时线程同时创建并各自初始化缓冲区再比如某个算法在峰值期一次性 malloc 了几百 MB。内存水位瞬间从 70% 冲到 99%内核的伙伴系统Buddy System在找连续内存页时反复失败最终触发重分配Direct Reclaim重分配还不行就进入了 OOM 判定流程。还有一类容易被忽略的情况内核自己也需要内存。比如页缓存被压缩、文件系统的元数据刷盘、网络收包缓冲区等。当系统剩余内存低到某个阈值以下内核连“自救”的余力都没有时它会比普通用户态进程更早地触发 OOM。这就是为什么有时候你看用户态程序占用的内存并不高但系统还是报 OOM——内核留给自己的那部分“安全垫”通常由vm.min_free_kbytes参数控制都没了。2.2 OOM Killer 如何决定杀谁OOM Killer 的决策并不随机它依据每个进程的oom_score来排序数值越高越容易被杀。默认情况下这个分数一大半来自进程的物理内存占用RSS进程占用内存越多oom_score就越高。另外系统会依据进程优先级nice值做微调优先级越低nice 数值越高的进程分数会稍微偏大。oom_score这个值可以在/proc/pid/oom_score里看到取值范围一般是 0 到 1000。你还能通过/proc/pid/oom_score_adj去手动干预——想保护某个关键进程就给它设一个负值比如echo -1000 /proc/pid/oom_score_adj表示“永不杀死”想让某个进程优先被牺牲就设一个正值比如echo 500 /proc/pid/oom_score_adj内核会把这个值直接加到oom_score上。有朋友会问既然/proc能改那是不是可以全系统把核心进程都设置成 “永不杀死”理论上是可行的实际中我强烈不建议全盘这么做。一旦触发 OOM一个候选进程都挑不出来内核就得面对一个致命选择题选不出要杀谁那就只能通过 Panic内核恐慌自杀式重启来保命。你要的是“瘫痪一个进程”不是“整台机器重启”所以oom_score_adj的调整要克制只针对真正需要保护的进程。提示oom_score_adj取值为 0 是默认最低可以到 -1000最高到 1000。设置 -1000 时进程几乎不可能被 OOM Killer 选中但要注意这不是绝对保险——有一种例外是内存不足已经严重到内核无法正常调度时一切保护机制都可能失效。2.3 一次完整的 OOM 生命周期把整条链路串起来看一次典型的 OOM 事件大体经过五个阶段某个或多个进程的物理内存使用量急剧上升剩余可用内存快速下降到水位阈值。内核触发内存回收直接回收或后台回收尝试把脏页写回磁盘、清空页缓存。回收速度跟不上分配速度内存请求反复失败内核进入“重分配”路径。系统判断已经没有进程愿意释放内存进入 OOM 判定OOM Killer 开始计算oom_score。选定并杀死目标进程向进程发送 SIGKILL同时在内核日志中记录一句Out of memory: Killed process ...。这条链路的每一环都有对应的观察手段。比如第二阶段的回收动作可以在/proc/vmstat里看到pgscan_direct和pgsteal_*这类计数器的跳变第四阶段是否进入 OOM可以通过dmesg的 “Out of memory” 文本判断也可以开启vm.oom_dump_tasks1让内核把每个进程的内存详情全部打印出来方便事后复盘。理解整条链条的核心意义在于你解决 OOM 的思路取决于它停在哪一环——如果是内存管理机制参数配置不当导致回收乏力调内核参数有效如果是某个进程本身存在内存泄漏调参数就是治标不治下必须换解法。把视角从“杀进程”抬升到“内存生命周期管理”后续所有工具和策略就有了正确的锚点。3. 现场急救OOM 发生后的第一反应3.1 快速判断“是不是 OOM”的硬指标我发现很多新人在服务挂掉的第一时间会先看free -h然后盯着 cached 那一列陷入沉思明明显示有几 G 可用为什么还 OOM这个误解的根源在于free对 available 的定义——available 是“估算的、能随时被回收给程序用的内存”它包含了大部分 page cache但 page cache 不是无限可用的内核需要保留一部分页缓存来保证文件系统性能。所以真正的可用内存其实比 available 显示的还要少一些。我的排查习惯是第一时间执行free -h看趋势然后立刻dmesg -T | tail -50。只要你在 dmesg 里看到类似Out of memory: Killed process的记录基本就能实锤是 OOM 干的。至于 “还剩几个 G 为什么会死”往往是因为那不过是可回收页缓存真正的匿名页进程堆栈和不可回收页已经把系统挤得捉襟见肘了。另外可以注意一个细节dmesg里如果出现大量 “page allocation failure” 或 “Out of memory: Kill process ... or sacrifice child”说明系统已经进入非常紧张的重分配循环。这类日志出现时就算当下还没杀进程也是即将 OOM 的前兆值得立刻处理。3.2 先止损快速释放内存空间确认是 OOM 之后的“黄金三分钟”我建议按以下顺序做杀掉非核心的高占用进程或容器把最占内存的无关服务停掉。可以用ps aux --sort-rss | head -20快速列出物理内存占用最高的前 20 个进程。检查 swap 是否为空。free -h看 swap 这行——如果 swap 还是 0说明系统压根没有 swap 或者根本没用到在内存告急的时候一个可用的 swap 可以充当缓冲垫给排查争取时间。如果系统已经处于“一杀进程就满一满就再杀”的抖动状态别犹豫直接把单机问题隔离出来先把关键流量切到备用节点再慢慢跑诊断。这三点里最难拿捏的是“杀哪个”。我遇到过业务方说“这个进程很重要别动”但那个进程实际上就是内存泄漏的元凶留着它整个机器都要跟着陪葬。所以我的原则是先统计内存占用再结合服务的实际角色来判断——凡是能够快速重启的服务优先级一律低于那些不能重启的核心服务。3.3 别急着重启先收集现场证据新手最常犯的一个错误是服务一挂立刻systemctl restart xxx先把服务重新拉起来再说。这本身没错但我强烈建议在重启之前花 30 秒把现场证据收集齐全否则之后想复盘问题日志已经滚动覆盖只能拍到脑门后悔。至少收集以下内容dmesg -T | tail -200内核日志里面包含被杀进程的名字、PID、total-vm、anon-rss等关键信息。cat /proc/meminfo和cat /proc/vmstat内存快照记录当前系统内存分布。ps aux --sort-rss | head -30进程内存占用快照找出当时的“大胃王”。如果系统启用了 systemd journaljournalctl -k -b还能搜到上一次启动时的内核日志。把这些存在一个文件里命名成oom_debug_时间戳.log。再顺手看一下dmesg里被杀进程的rss总量对比它正常运行时的大小如果被杀时 RSS 是平时的 3 倍以上大概率这就是元凶。注意重启会清空大部分运行时信息/proc下的很多动态指标也会归零。所以“先取证、再重启”的顺序一定不能反过来。4. 从日志读出门道OOM 信息的逐行解读4.1 dmesg 里那几行要命的日志很多新手看到 dmesg 里的 OOM 日志就是一行“Out of memory: Killed process”然后就没有然后了。但如果内核开启了完整 dump很多发行版默认关闭可以通过vm.oom_dump_tasks1开启你会看到一段非常详细的信息逐行拆开看非常有价值。典型日志结构大致如下java: invoked oom-killer: gfp_mask0x4000c0, order0, oom_score_adj0 java: cpuset/ mems_allowed0 CPU: X PID: 1234 Comm: java ... [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [ 1234] 0 1234 3712345 123456 ... 0 0 java [ 5678] 0 5678 4567890 98765 ... 0 0 python3第一行里invoked oom-killer表示哪个进程把内核拽进了 OOM 判定流程通常是“内存分配请求方”但不一定是要被杀的那个。gfp_mask是内存分配的标志位集合order0表示分配的是单个内存页如果order大于 0说明是在申请连续物理页块这种情况往往是内存碎片化导致的。真正有用的信息在[ pid ]开始的进程列表里面。rss列是进程的物理内存占用量单位是页乘以 4KB 就是实际大小total_vm是虚拟内存总大小name是进程名字。对比几个候选进程的rss你能非常直观地看出当时谁吃内存最多、被杀的为什么是它。4.2 从日志反推触发场景OOM 日志里还有一个信息容易被忽视Mem-Info那一段。它会把当前的内存情况列出来包括active_anon、inactive_anon、active_file、inactive_file等。我一般会重点看active_anon和active_file的比例如果active_anon飙升说明匿名页程序堆、栈占用极高大概率是用户态应用的问题如果active_file飙升说明页缓存压力很大可能是突发大量读写导致缓存暴涨。举个例子我曾经排查过一个“每天凌晨三点定时死机”的诡异问题。从 dmesg 的 Mem-Info 看到active_file异常高顺着查才发现是凌晨的日志归档任务产生了大量读文件操作把页缓存直接顶满。问题解决后我顺手给日志归档加了 cgroup 内存限制再也没出过事。这就是读日志读出来的价值——如果不看 Mem-Info我可能还在傻乎乎地加内存条。4.3 内核日志里容易忽略的伴生信号OOM 通常不是孤零零发生的它会伴随一系列其他内核告警。常见的有page allocation failure内存分配请求失败但还没进入最终 OOM 阶段。INFO: task ... blocked for more than 120 seconds进程因为等待内存分配而长时间阻塞说明系统内存压力已经非常严重。hung_task_timeout_secs触发的 hung task 告警常常和内存分配死锁伴生。这几种日志在 dmesg 里出现的顺序往往能帮你还原事件的“时间线”先有 allocation failure再出现等待阻塞最终触发 OOM。看到这条链基本就能确定是分配压力持续叠加导致的问题而不是某个瞬间的巧合。5. 排查内存峰值的长效工具与手段5.1 用好系统自带的轻量工具长期排查内存问题我习惯组合使用几个轻量工具不需要额外安装什么重型监控先把基本盘摸清ps aux --sort-rss看当前进程的实时物理内存占用一秒钟出结果。free -h看系统整体内存水位重点关注 available 和 swap。cat /proc/meminfo细看内存组成部分包括MemTotal、MemAvailable、SReclaimable、Slab等。top/htop观察动态变化能持续看几秒到几分钟找到内存涨幅最快的进程。cat /proc/pid/status看单个进程的详细内存字段其中VmRSS是它当前的物理内存占用VmSize是虚拟内存大小。注意区分这两个很多新手拿VmSize当成物理内存去排查方向就偏了。另外/proc/pressure/memory文件记录了内存压力的 PSIPressure Stall Information数据包括some和full两个维度。some表示有些任务在等待内存full表示所有任务都在等。这个指标能帮你量化“内存吃紧到什么程度”结果比单纯的百分比更直观。5.2 统计峰值内存的三板斧项目里的临时问题排查完之后还是要面对一个本质问题峰值内存到底多大稳定复现了吗我用过三种比较可行的手段第一种ps定时采样。写一个简单的 while 循环每 2 秒把ps -eo pid,comm,rss输出到文件跑上半个小时再用 awk 统计每个进程 RSS 的最大值和平均值。这个方法简单、零侵入适合临时应急。第二种使用systemd-cgtop或cgroup的memory.peak计数。如果你的服务跑在 systemd 或容器里cgroup v2 提供了memory.peak这个只读文件记录该 cgroup 历史最高内存用量。一行cat /sys/fs/cgroup/路径/memory.peak就能拿到自启动以来的峰值这是最准确也最省事的办法强烈推荐优先使用。第三种临时开启内核的perf内存事件或者用valgrind的 massif 工具对你怀疑的进程做详细内存画像。这个方法开销大、周期长一般只在确认某个进程存在内存泄漏后才会用。5.3 识别“看似正常实际异常”的进程有些进程隐藏得很深它们平时 RSS 不高但偶尔瞬间冲高比如 Java 的 Eden 区、Python 的临时大列表、Node 的一次全量 GC 等等。这类“脉冲式内存”很难在事后复盘时捕获因为 OOM 发生时进程已经被杀掉了原始证据只存在于日志里。对于这类问题我的经验是“两查一防”查日志里被杀进程的total-vm和rss比值如果total-vm极大而rss相对较小说明进程申请的逻辑内存很多但实际写入的少可能是虚拟内存无限膨胀型问题一防是给这类进程提前设置内存上限让它在膨胀到失控前先被自己的限制拦住而不是拖垮整机。6. 从根上解决问题一套可落地的止血流程6.1 评估要加内存还是调服务“OOM 最佳解决方案”没有统一答案而是要分场景判断。如果一台服务器上跑的服务内存总量本来就接近物理上限长期使用率经常在 85% 以上那我建议直接加内存或拆分服务因为降负载是系统性的靠调参数只能短暂缓解。如果服务本身存在无界缓存、大集合长期不释放、连接池过度膨胀这类问题光加内存就是把问题挪到更大的内存条上重演一遍。这时候应该去代码或配置层面找到那个“无限扩张”的源头比如把 HashMap 缓存改成带过期策略的本地缓存把 JDBC 连接池的 maxActive 设置到一个合理水平。判断依据我给三条参考内存长期平均使用率是否超过 85%是 → 扩容或拆分。峰值内存与均值内存的差异是否超过 2 倍是 → 服务内部存在瞬时大对象或缓存堆叠。同一进程反复成为 OOM 被杀对象是 → 大概率是应用层问题而不是系统配置问题。6.2 给关键进程套上安全绳swap 与 cgroup在应用侧彻底修复之前先把安全绳挂上。第一根安全绳是 swap。很多系统为了性能直接关闭了 swap但代价是内存进入紧急状态时毫无缓冲。我建议在物理内存允许的前提下分配一个大小约等于物理内存 20%~50% 的 swap 分区或 swap 文件。它真正的价值不是让你“用内存换性能”而是给 OOM 判定提供一个缓冲时间窗口让你有足够的时间介入处理。第二根安全绳是cgroup内存限制。给每个业务进程或容器设置独立的内存上限把“内存爆炸”的爆炸半径限制在单个服务内。比如 Docker 容器设置--memory2gsystemd 服务设置MemoryMax2G这样某个服务的泄漏就不会拖垮整台宿主机。这是我在生产环境中最推荐的防守性配置成本极低、效果立竿见影。6.3 内核参数的合理调优清单内核参数不是调剂神器但要本着“少调、精准调”的思路来做。以我自己的经验来看下面几个参数在 OOM 问题上最值得关注参数默认值调整建议适用场景vm.overcommit_memory0启发式1 或 2系统经常因突发申请而 OOM可设置 2 加大保守程度禁止超卖vm.overcommit_ratio50按需调整配合overcommit_memory2使用控制可超卖比例vm.panic_on_oom0默认保持 0设置为 1 意味着 OOM 时直接内核 panic 重启生产环境慎用vm.oom_kill_allocating_task01直接杀掉触发 OOM 的进程而不是系统自动挑选vm.min_free_kbytes动态适当调大内核保留的安全内存垫调大能减少频繁进入直接回收的几率vm.swappiness600~10 或按需调整匿名页回收与页缓存的偏好这里特别提醒一点vm.panic_on_oom这个参数经常被误配置。有些人为了“OOM 时自动重启”把它设成 1结果本来只是杀掉一个进程就能恢复的问题变成整机重启业务中断时间反而更长。我见过最合理的用法是设置为 0让内核自动找进程杀掉再用 node_exporter 之类的监控盯住节点状态。6.4 应用层的自我调节系统层兜底之外应用侧也要有自我调节机制。以 Java 为例配置正确的堆大小是前提-Xmx不要设成物理内存的全部留 20% 以上给系统页缓存和其他进程同时开启-XX:ExitOnOutOfMemoryError让 JVM 在堆 OOM 时直接退出进程而不是无限 GC 阻塞线程——这样上层编排系统K8s 或 systemd能更快感知并重启服务。对 Python、Node、Go 这类应用重点在于限制并发和缓冲连接数限制、队列长度上限、缓存过期时间都应该设一个明确的上限。很多 OOM 的根本原因就是“代码里有一处没有边界的缓存或列表”没有任何调参能补救无限增长的数据结构。7. 常见问题速查与深度避坑7.1 高频问题排查速查表现象可能原因首选排查路径服务被杀但free -h显示还有内存可回收页缓存掩盖了真实内存压力/proc/meminfo中查看MemAvailable和active_anon每次 OOM 杀的都是同一个进程该进程存在内存泄漏或无界缓存对该进程做长时间监控 查看内存峰值多个进程同时被杀系统内存整体耗尽OOM 一次性挑了一批检查是否突然有大流量或大面积任务并发杀完进程后系统依然卡顿内存反复紧张回收线程在持续工作查看vmstat的si、so和 CPU 的 iowait容器被杀宿主机完好cgroup 内存限制生效查看容器日志和memory.events中的oom记录OOM 日志出现频繁但无规律可能是瞬时大对象集中分配统计峰值内存分析应用 GC 或任务周期7.2 swap 的脏坑与正确用法swap 不是万能药这里有个脏坑如果磁盘 I/O 能力差大量使用 swap 反而会让系统更慢甚至因为 swap 读写卡顿导致进程远超预期地阻塞。我建议把“swap 是否该开”和“swap 开多大”当成两个问题来看——前者决定缓冲垫有没有后者决定缓冲垫多大、多快。在 SSD 或 NVMe 上开 swap 基本无感在老旧机械硬盘上要谨慎。另外一个常见问题是很多人开了 swap 但vm.swappiness0这几乎等于没开。swappiness控制的是内核在回收内存时偏好回收匿名页进入 swap还是页缓存如果设为 0内核会优先回收页缓存在缓存极少时才会碰 swap。我的建议是保留默认 60 或者在内存压力大的机器上设 10 左右确保紧急情况下 swap 真的能派上用场。7.3 容器场景下的 OOM 误判如果你用 Docker 或 Kubernetes很容易把宿主机 OOM 和容器 OOM 搞混。容器 OOM 往往不是因为宿主机内存不够而是容器的 cgroup 内存限制到了。区分方法很简单dmesg里如果出现宿主机 OOM 日志说明整机都受影响了如果只有容器被杀而宿主机正常去容器目录下的memory.events看oom计数是否大于 0。容器场景里还有一个典型坑内存 limit 配置不正确。limit 低于应用实际运行需求服务一启动就被 CGroup OOM Killer 杀掉看起来像“系统不稳定”实则是配置失当。这种问题最好通过设置memory.peak监控或kubectl describe pod观察 events 来定位。7.4 事后复盘的三板斧问题解决了不等于结束。我建议每次都做一个简短复盘内容围绕三条主线触发点当时什么操作或流量变化把内存顶到了极限放大点为什么系统没能更早介入是警阈值太高还是监控缺失阻断点后续如何做才能让同样的问题不再发生或者发生时影响降到最小。在此基础上把这次 OOM 的核心内存数据峰值、触发进程、系统参数记录下来建立一个“内存事件档案”。长期积累下来你会发现自己对系统内存的判断力提升很快——很多问题在发生之前就已经能预判了。8. 综合复盘一个模拟项目 X 的 OOM 处理全流程为了把前面所有内容串起来我用最近刚处理完的一个模拟项目 X 的具体场景做一次完整复盘。这个项目的服务部署在一台 8 GB 内存的服务器上跑着一个 Web 服务和一个定时任务进程。第一天深夜监控告警弹出“进程失联”我登录服务器后按顺序做了下面几件事第一步free -h看到 available 只剩 320 MBswap 为 0。第二步dmesg -T | tail -100看到 OOM Killer 把定时任务进程杀了日志里显示该进程的rss达到了 4.7 GB而平时它只有 800 MB。第三步cat /proc/meminfo看到MemAvailable极低Slab也偏高。第四步ps aux --sort-rss | head -20确认 Web 服务本身只占 1.8 GB问题不大。初步判断是定时任务进程在夜里某次运行中产生了海量临时对象内存瞬间飙升。于是我先给这个服务配置了MemoryMax5G的 systemd 限制同时开启了一个 2 GB 的 swap 文件做缓冲防止再出现问题时直接杀进程。接下来我在应用的日志采集侧加了分段导出逻辑把一次性全量大对象拆成小块分批处理内存峰值从 4.7 GB 降到了 1.5 GB 左右。最后我在监控系统里加了memory.peak的每日记录和进程 RSS 的 5 分钟平均采样再遇到类似问题就不再是“深夜爬起来应急”而是早上打开面板看数据就能定位根源。整个过程从出现告警到问题基本落地解决花了不到两天核心方法论就是本文提到的先取证、再隔离、后优化、上监控。9. 几句实操心得文章写到这里核心内容已经全部铺开。最后我再分享几个只有自己亲手调过 OOM 才会有的真实体会。第一OOM 不是“内存不够用”那么简单它反映的是整个系统的内存水位管理是否健康。只看free -h的表层数字往往会漏掉真正的风险。养成看/proc/meminfo、vmstat、dmesg这三件套的习惯比什么监控工具都靠谱。第二参数调优永远是最后一步不是第一步。先把业务侧的无界缓存、超大堆、失控连接数查干净再把内核参数作为补充手段来微调。顺序反了你会在错误的方向上消耗大量时间。第三每个系统的 OOM 特征都不完全一样。有人的服务是被瞬时大对象打死有人的是被某个隐藏的线程池撑爆有人的是 swap 配置形同虚设。与其照搬别人的配置不如花时间把你自己的 OOM 日志读细、读透。我遇到过一次特别隐晦的情况OOM 日志显示被杀的是一个无关紧要的后台进程但真正的问题在另一个进程的内核线程上——如果不看 Mem-Info 那一段怎么都猜不到。第四容器环境下的内存治理和裸机不完全一样。cgroup 限制既是保护也是陷阱——保护是爆炸半径可控陷阱是容器内的应用可能根本不知道自己的内存上限是多少配置错得离谱却归咎于系统不稳定。给容器应用加内存参数校验和启动期自检能省掉大量莫名其妙的排查时间。最后想说的是OOM 这件事看着吓人其实本质就是“内存管理中的一次强制淘汰”。把它当成一次系统健康的体检反而能帮你发现自己平时注意不到的隐患。我后面还会单独写一篇关于 cgroup v2 内存水位控制与 PSI 预警的进阶内容如果这篇文章对你有帮助欢迎关注后续更新。