Linux物理内存耗尽OOM排查实战:从日志到根因定位 物理内存耗尽OOM排查实战记录作为一个经常跟 Linux 服务器打交道的开发者我最怕的就是半夜被电话叫醒说业务挂了。而业务挂掉的大部分原因排在第一位的往往不是代码 bug而是内存问题。尤其是那种内存缓慢增长一直到某个临界点突然 OOM 的情况排查起来极其折磨人——进程在服务没报错日志里只有一行冰冷的 Out of memory: Killed process。这一篇笔记就是我踩了无数坑之后整理出来的内存排查思路和实操记录。不聊虚的全是命令、参数、思路和真实场景。不管你是刚入门 Linux 的运维小白还是写后端服务的开发同学只要你需要跟服务器内存打交道这篇内容都能帮你少走几周弯路。1. 从一次线上 OOM 开始说起事情是这样的。某天下午一个内部业务系统突然出现大量超时报警登录服务器一看dmesg里躺着一行致命日志[892341.221045] Out of memory: Killed process 22341 (java) total-vm:8423012kB, anon-rss:6120332kB, file-rss:0kB, shmem-rss:0kB当时的第一反应是 Java 堆内存配置太大了马上看了下启动参数-Xmx4g配置本身没问题。然后 free -h 一看有点不对劲total used free shared buff/cache available Mem: 15G 11G 1.2G 1.8G 3.5G 3.2G内存确实快没了但总量 15GJava 堆才 4G还有 7G 被谁吃掉了也就是说真正的问题根本不在于 Java 堆配置而是有什么东西在疯狂占用堆外内存。后来逐步排查发现是在一个第三方 SDK 里底层用 JNI 调用了本地方法每次请求都分配一块 C 层的内存缓冲区用完之后没有及时释放日积月累堆外内存越涨越高最终把整台机器的内存耗尽了。这个案例特别典型因为它反映了一个非常常见的认知误区说到内存问题很多人的第一反应就是堆内存、垃圾回收实际上服务器内存是一个整体除了堆还有堆外、内核、页缓存、共享内存等各种区域。如果你只盯着某一层去查往往会陷入死胡同。所以这篇文章我不会只讲某一个工具或某一条命令而是把我自己在排查内存问题时的一套完整思路、工具组合和判断方法分享出来尽量还原真实场景。1.1 内存问题为什么这么难查内存问题难主要有三个原因。第一现象有滞后性。CPU 问题往往是一个请求突然变慢、一个线程突然飙高能被立刻感知到。但内存泄漏是缓慢的今天内存用量涨了 2%明天涨了 1.5%你要是不盯着监控曲线很难察觉到异常。等到 OOM 发生的时候现场已经被破坏了一大半——进程被杀、日志可能没来得及落盘、缓存被清空很多线索都没了。第二内存的构成太复杂。一台 Linux 服务器的内存由进程的用户态内存、内核内存、页缓存、临时文件tmpfs、共享内存等组成。每个进程内部又分堆、栈、代码段、数据段、各种映射区域。你用 free 看到的是整体用 top 看到的是进程级用 jstat 看到的是 JVM 级每一层看到的东西都不一样。如果没有一个整体框架很容易被某一层的数据误导。第三工具输出容易让人误判。举个例子top 里的 RES 列很多人以为这就是进程真正使用的物理内存实际上 RES 包含了共享库、页缓存映射等不完全等于这个进程独占的物理内存。同样free 里的 buff/cache 看起来被用了实际上它是可以回收的真正需要关注的是 available。1.2 一个可复用的排查框架我自己在实战中把内存排查拆成四个阶段现象确认确认是不是真的内存不足判断是系统级还是进程级问题。全局定位看整机的内存分布搞清楚内存都被谁消耗了。进程深挖锁定嫌疑进程深入分析它内部的内存结构。根因定位结合代码、日志、分配器行为找到最终原因并验证。这四个阶段不是绝对的有时需要反复横跳。比如你深挖进程时发现这个进程内存正常但整机内存还是不够那就得回到全局重新分析。下面我按这个框架把每个阶段用到的命令、判断方法和注意事项一个个说清楚。2. 现象确认阶段别急着背锅很多人一上来就free -htop然后就开始猜。我的建议是先花两分钟把现象弄清楚尤其是把 OOM 日志、监控曲线、最近变更这三样东西凑齐。2.1 第一步查 OOM 日志Linux 的 OOM Killer 在杀进程时会打印详细的日志这些日志藏在dmesg或者/var/log/messages里。用下面的命令快速定位dmesg -T | grep -i out of memory | tail -20如果dmesg需要 root 权限看不到可以试试journalctl -k --since 2024-01-01 00:00:00 | grep -i out of memory拿到日志后重点看两样东西一是被杀的进程是谁二是当时系统的内存压力指标。注意OOM 日志里经常出现被杀进程看起来不是内存占用最大者的情况这是因为 OOM Killer 选择目标时会综合考虑进程的占用、运行时长、优先级oom_score_adj等不一定是占用最高的那个这个细节后面会展开。2.2 第二步看监控曲线如果没有监控系统至少也得把/var/log/sysstat/里的 sar 历史数据翻出来看看sar -r -f /var/log/sysstat/sa$(date %d) | more重点看内存使用趋势是平稳的还是稳步上升的。如果是锯齿状的一升一降大概率是正常业务波动如果是持续上升、而且每次上升的底部都在抬高那基本就是内存泄漏的形状。还有一种情况是陡增型内存瞬间被打满这往往是流量突增、缓存被大量填充、或者出现了类似一次性加载大量数据到内存的操作。2.3 第三步确认最近变更排查内存问题有一条血泪教训先问变更再查代码。很多内存问题并不是逐渐积累的而是某次发布、某个配置调整之后突然出现的。比如线程池调大、连接池参数改了一个数字、上线了新功能都可能让内存水位明显变化。我的习惯是把以下信息列成一个清单最近一次发布是什么时候代码变更涉及哪些模块是否有配置变更比如 JVM 参数、内核参数vm.overcommit_memory、vm.swappiness是否有流量变化比如大促、爬虫高峰期、新渠道接入这三样信息凑齐之后再进入下一阶段。3. 全局定位阶段看清整机内存地图这个阶段的目标是回答一个问题16G 的机器内存到底被谁拿走了3.1 free 命令的正确读法先纠正一个常见误区。很多人看 free 只看 used 一列发现 used 很高就慌了。其实 free 命令最有价值的参数是free -h而且重点要看 available 这一列total used free shared buff/cache available Mem: 15G 11G 1.2G 1.8G 3.5G 3.2Gavailable 是在不触发 swap 的情况下预计可以分配给新进程的内存大小。它已经扣除了内核估算的不可回收部分同时也把可以回收的 page cache 计算在内了。如果 available 长期很低比如只有几百 MB即使 free 显示还有 1G也应该警惕。再解释一下 buff/cache。这部分内存是 Linux 用空闲内存来缓存磁盘数据、加速 IO 的属于用掉但可以吐出来的内存。正常情况下不用管它但如果你的应用对 IO 有极致要求或者机器本身内存就小可以适当调整vm.dirty_ratio、vm.dirty_background_ratio等参数但千万不要在没搞清楚问题之前就去 echo 3 /proc/sys/vm/drop_caches这只能让下次 IO 变慢解决不了内存泄漏。3.2 用 /proc/meminfo 查看内核视角free 命令的数据其实来自 /proc/meminfo想看更细的信息可以直接看这个文件cat /proc/meminfo | head -30重点关注几个指标MemTotal、MemFree、MemAvailableBuffers、Cached、SwapCachedAnonPages匿名页通常是进程的堆、栈、匿名 mmap这是内存的大头Mapped文件映射到内存的大小Slab、SReclaimable、SUnreclaim内核对象占用的内存SUnreclaim 如果异常增长往往是内核态内存泄漏的信号Shmem共享内存包括 tmpfs 文件占用的内存有一个场景我印象特别深某次排查发现 free 显示的 used 不高但系统总是频繁 OOM后来发现 tmpfs 挂载目录下有人往里面写了大量临时文件Shmem 占到 4G 多而这些内存无法被回收最终导致了内存耗尽。这种问题用 top 是看不出来的必须查 /proc/meminfo。3.3 用 ps 快速揪出内存大头拿到整体视图后下一步就是看哪些进程占用最多ps aux --sort-%mem | head -20或者更精确一点直接看 RSSResident Set Size常驻内存ps -eo pid,ppid,rss,vsz,comm --sort-rss | head -20这个命令的输出里RSS 单位是 KB看的时候心里要换算一下。如果头部的几个进程加起来离整机内存还有很大距离说明内存消耗在进程之外比如 Slab、Shmem、内核占用这时候就别在进程列表里继续浪费时间了回到 /proc/meminfo 去找线索。4. 进程深挖阶段揪出进程里的内存黑洞如果已经锁定某个进程接下来要弄清楚这个进程内部的内存是怎么分布的。不同语言写的程序分析方法完全不同。我以最常见的 Java 服务、C/C 服务和 Python 服务为例分别讲。4.1 看进程内存布局/proc/PID/status 和 smaps先看进程的整体情况cat /proc/12345/status | grep -E VmPeak|VmSize|VmRSS|VmData|VmStk|VmExe|VmLib几个指标的含义VmSize进程虚拟内存大小VmRSS常驻物理内存大小VmData堆和匿名映射占用的虚拟内存VmStk栈大小VmLib共享库占用的虚拟内存这里有个很重要的经验如果 VmSize 很大但 VmRSS 很小说明进程只是虚拟内存分配得多实际上没有真正占用物理内存这种情况一般不需要太紧张。真正危险的是 VmRSS 持续走高。如果要看更细粒度的内存分布可以看 smapsgrep -E Rss:|Pss:|Size: /proc/12345/smaps | head -60smaps 里每一段映射都有详细信息。PSSProportional Set Size是个好东西它把共享库占用的内存按比例分摊到每个进程上比 RSS 更能反映这个进程真正分摊了多少物理内存。4.2 Java 进程别只看堆Java 服务的内存排查有个经典误区只盯着 Xmx 和堆内存。实际上一个 Java 进程的完整内存 堆内存 线程栈 Metaspace 直接内存Direct Buffer JIT 编译器代码缓存 JNI/本地内存 GC 数据结构 各种 native 库。我的排查步骤是第一步看 JVM 堆内使用情况jstat -gcutil 12345 1000 10重点关注 EEden、OOld、MMetaspace如果 Old 区持续占满且 FGC 频繁说明堆内有问题如果堆内一切正常但进程 RSS 还是很高问题大概率在堆外。第二步看堆外内存的大头——线程栈和 Direct Bufferjcmd 12345 Thread.print | grep java.lang.Thread.State | wc -l jcmd 12345 VM.native_memory summaryVM.native_memory summary输出里会有 Java Heap、Thread、Code Cache、GC、Compiler、Internal、Symbol 等各个区域的 native 内存占用方便定位是不是线程数异常或者某个 native 区域在疯涨。注意这个命令需要启动参数里加上-XX:NativeMemoryTrackingsummary否则输出有限。第三步如果怀疑是 Direct Buffer可以手动触发几次 GC 然后观察进程 RSS 是否下降jcmd 12345 GC.run如果 GC 之后 RSS 明显下降说明有堆外内存中可以被 GC 回收的部分在作祟如果 GC 后 RSS 纹丝不动那大概率是纯 native 泄漏GC 帮不上忙。4.3 C/C 程序用 pmap 和 malloc 调试点C/C 程序的内存问题相对更直接一些pmap 可以看进程的内存映射pmap -x 12345 | tail -30关注几类映射heap堆、anon匿名映射、libs共享库。如果 anon 区域很大且持续增长说明有动态分配的内存在积累。另外编译期加上 AddressSanitizer 或者 Valgrind 可以帮助定位具体是哪一行代码泄漏的valgrind --leak-checkfull --show-leak-kindsall ./your_program不过 Valgrind 会让程序慢上几十倍生产环境不好直接跑通常是在测试环境复现问题后再用。4.4 Python 进程tracemalloc 和 objgraphPython 的内存泄漏排查最常用的就是内置的 tracemalloc 模块import tracemalloc tracemalloc.start() # 业务代码跑一段时间后快照对比 snapshot1 tracemalloc.take_snapshot() # 运行一段时间... snapshot2 tracemalloc.take_snapshot() top_stats snapshot2.compare_to(snapshot1, lineno) for stat in top_stats[:10]: print(stat)它能定位到泄漏对象是哪个文件哪一行代码创建的排查业务代码非常有效。如果泄漏是发生在 C 扩展层tracemalloc 就无能为力了这时候可以借助 objgraph 分析对象引用关系看看是哪些对象互相持有引用导致无法被回收。5. 根因定位从 内存高 到 谁导致的前面几层分析可以定位到进程的哪个区域内存高但还不够必须定位到哪一行代码、哪一个逻辑导致的高。这一阶段最依赖经验也是最难的部分。5.1 常见根因模式根据我自己的经验内存泄漏和高内存占用大多逃不出这几类集合类未清理全局 Map、List 不断添加元素尤其是用静态集合做缓存又没设上限的。连接和会话对象未关闭数据库连接、网络连接、文件流用完没关底层对应的缓冲区内存一直挂着。本地缓存无限增长各种 Guava Cache、Caffeine Cache 如果没设置 maximumSize 和过期策略就会一直膨胀。线程数量异常线程栈默认 1MB1000 个线程就是 1G 虚拟内存再加上每个线程的内部缓冲区数量失控立刻反映到 RSS 上。第三方 SDK 的 native 泄漏回顾开头提到的 JNI 场景进程 RSS 高但堆内、线程数都正常时要想想是不是有 native 库在背景里分配内存。JVM 元空间泄漏动态创建类、频繁生成代理类Metaspace 会不断增长。5.2 一个真实案例缓存导致的内存膨胀某次排查一个报表服务发现 RSS 从 2G 逐步涨到 6G。堆内存 jstat 看下来Old 区一直 60% 上下FGC 也不算频繁显然问题不在堆内。于是用jcmd 12345 GC.class_histogram看对象分布发现有个com.xxx.ReportCacheNode对象数量异常多有几十万个。再配合jmap -histo:live 12345 | head -30确认就是它。最后定位到代码里用了一个静态HashMap做报表结果缓存每次查询结果都不一样导致 key 永远匹配不上旧数据永远无法淘汰。修复方案就是引入 Caffeine Cache设置 maximumSize 和 expireAfterWrite内存立刻稳住了。5.3 内核态内存泄漏的排查如果进程层排查完都没问题整机内存还是被吃要考虑内核态的问题。SUnreclaim不可回收的 Slab持续增长是一个典型信号cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim如果 SUnreclaim 持续增长可以用 slabtop 看看是哪类内核对象在膨胀slabtop -o | head -20常见的可疑对象包括dentry文件目录项缓存、inode_cache、filp打开的文件对象、kmalloc-*内核小对象分配。dentry 和 inode 异常增长通常和某个目录下文件数量过多或频繁创建删除文件有关kmalloc 对象无故增长则要警惕内核模块或者驱动泄漏。遇到这类问题通常需要结合内核版本的已知 bug 列表来排查必要时升级内核。6. 常用工具清单与核心参数速查工具多而不精等于没有工具。我在实际工作中长期使用的就是下面这张表按场景分类可以直接抄作业。6.1 系统级工具工具用途关键参数/命令free查看整机内存free -h重点看 availabletop/htop动态查看进程内存与 CPUtop -o %MEM按内存排序vmstat查看内存、swap、IO 的整体状态vmstat 1 5关注 si/sosar历史内存记录sar -r -f 某天的sa文件/proc/meminfo内核视角的内存分布关注 AnonPages、Slab、Shmemdmesg查看 OOM 日志dmesg -T | grep -i out of memory6.2 进程级工具工具用途关键参数/命令ps进程占用排序ps aux --sort-%mempmap进程内存映射细节pmap -x PID/proc/PID/smaps每一段映射的内存明细查看 Pss、RssjstatJVM 堆内统计jstat -gcutil PID 1000jmapJVM 堆转储与对象统计jmap -histo:live PIDjcmdJVM 综合诊断jcmd PID VM.native_memory summaryvalgrindC/C 内存检测valgrind --leak-checkfulltracemallocPython 内存追踪内置模块快照对比slabtop内核 Slab 对象slabtop -o6.3 swap 参数什么时候该改vm.swappiness控制内核优先使用 swap 还是回收页缓存的倾向默认通常是 60。对于延迟敏感的服务我一般会调到 1~10让内核尽量少用 swap对于批处理任务反而不用刻意调低因为偶尔的 swap 能缓解瞬时内存压力。修改方式sysctl -w vm.swappiness10 echo vm.swappiness 10 /etc/sysctl.conf注意swappiness 调到 0 不代表完全不用 swap在紧急内存压力下内核仍可能 swap 出去。真正想完全禁用 swap 要在启动参数里设置swapaccount1或修改 grub 参数但这个操作会影响系统行为不建议在生产环境随意搞。另一个关键参数是vm.overcommit_memory。默认 0 表示内核做启发式过度分配如果改成 1就允许进程申请超过物理内存的虚拟内存这会让 OOM 的风险大幅上升改成 2 则严格执行 overcommit 限制。除非你明确知道自己在做什么否则不要动这个参数。我之前遇到过有人把 overcommit 改成 1 之后Java 进程虚拟内存申请不受限实际物理内存不够时 OOM 频率翻倍最后又改回来了。7. 一次完整排查的演示从日志到结论为了让你把前面所有的工具串联起来我用一个虚构但非常典型的场景把完整流程走一遍。7.1 场景描述假设你是一个中间件平台的负责人某个网关服务最近几天频繁出现告警响应超时率上升部分节点 OOM。服务是 Java 写的部署在 8C16G 的容器里JVM 参数是-Xmx4g -Xms4g -XX:UseG1GC。7.2 阶段一现象确认登录一台还在运行的节点先看 dmesgdmesg -T | grep -i out of memory | tail -5没有 OOM 记录说明容器层面可能已经把进程 kill 了而宿主机的 dmesg 看不到需要看容器侧日志。再看监控曲线发现这个服务的内存在每天凌晨 3 点到 4 点之间开始爬坡早上 8 点回落到正常水位。但最近一周回落的底部越来越高从 3G 涨到 5G。7.3 阶段二全局定位free -h输出显示total 16Gused 13Gavailable 只有 1.6G。再看看 /proc/meminfo发现 AnonPages 有 10GShmem 只有几百 MSlab 也正常。基本可以确认内存大头在进程的匿名页上跟内核缓存无关。7.4 阶段三进程深挖用 ps 排序看到最前面的是一个 Java 进程 RSS 6G另一个 Java 进程 RSS 4.5G。于是把嫌疑集中在占用最高的网关进程上。进入进程内部用 jstat 看堆jstat -gcutil 12345 1000 5输出显示 Old 区使用率 70%FGC 每分钟 2 次但 RSS 还在增长。再用 jcmd 看 native memoryjcmd 12345 VM.native_memory summary结果中 Thread 区域占用 1.5G说明线程数量非常多。用jcmd 12345 Thread.print | grep java.lang.Thread.State | wc -l数了下居然有 1800 个线程而合理值应该在 400 左右。7.5 阶段四根因定位线程数量异常马上查代码发现某个 HTTP 调用没有设置连接超时和读超时部分上游服务响应变慢时请求线程全部阻塞在 IO 上线程池不断创建新线程每个新线程 1MB 栈加上内部缓冲区内存立刻飙升。修复方案是给 HttpClient 设置连接超时和读超时并给线程池设置一个合理上限和拒绝策略。上线后内存曲线很快就平了。这个案例想说明的是内存问题往往是另一个问题的果。线程数飙升是果IO 阻塞才是因。所以当内存异常时不要只盯着内存要顺着调用链路把所有不正常的东西串起来看。8. 这几个月踩过的坑和值得记住的经验最后集中写一些我踩过坑之后最想告诉你的经验这些内容在很多书和文档里不太会写。8.1 生产环境千万别乱执行这 3 个命令kill -9 一个你还没搞清楚的进程除非你想快速止血否则在 dump 或收集完现场信息之前不要轻易 kill。进程一没很多证据就没了。drop_caches 清缓存echo 3 /proc/sys/vm/drop_caches虽然能清理 page cache但它只是让你感觉内存变多了并不能定位根因反而可能让下一次 IO 变慢。生产环境直接开 NMTNative Memory Tracking-XX:NativeMemoryTrackingsummary本身会带来 5%~10% 的性能开销如果是在高并发大流量环境要评估好再开最好先在压测环境验证。8.2 观察内存不要只看一个点内存问题是需要趋势视角的。单次 top 只能告诉你现在有多紧张但无法告诉你它是一路涨上来的还是刚刚才涨的。所以我强烈建议在服务器上常驻一套监控哪怕只是最简单的sar -r定时采集配合一个脚本每 5 分钟把/proc/meminfo和进程 RSS Top10 追加到日志文件里等出问题的时候有数据可看。8.3 堆内堆外要联合看Java 服务的内存问题90% 不是简单的-Xmx调大调小能解决的。堆内满了可能是对象太多堆外长了可能是线程、DirectBuffer、native 库。我见过太多人一遇到 Java OOM 就调大 Xmx结果把整机内存调爆。正确做法是先搞清楚到底是堆内还是堆外再对症下药。8.4 把 OOM 变成可观测的工具再强也不如提前预防。我给自己定了几条硬规矩所有 Java 服务启动参数里都加上-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath/data/logs/heapdump.hprof确保 OOM 时能留下堆转储。容器启动时给 JVM 预留足够的非堆内存空间计算公式通常是最小堆 线程数×1MB Metaspace 直接内存 200MB 余量。监控上设置内存水位的分级告警比如使用率超过 70% 时 P3 提醒超过 85% 时 P1 告警避免每次都是 OOM 了才被机器通知。8.5 最后一句话内存排查这行当最值钱的不是命令记得有多熟而是脑子里有没有一张内存地图。先从 /proc/meminfo 建立全局认知再一层层往下钻用工具验证假设最后在代码里找根因。这套动作多练几次你会发现自己越来越能从一串冷冰冰的数字里读出真正的问题。最后再分享一个小技巧排查内存问题时记得把每次排查的命令、输出和结论记下来哪怕是简单地贴在笔记本里。时间久了你会建立起自己的内存问题模式库下次遇到类似现象时直接就能从历史案例里找到方向。这比任何工具都管用。