服务器内存告警又响了,free一看使用率 99%,业务进程一个接一个被 OOM Killer 点名,半夜爬起来救火的滋味,干运维的都懂。这种场景我处理过太多次,第一次遇到时也慌,差点直接重启大法,后来才知道问题根本没解决,该占的内存照样占。这篇把排查 Linux 服务器内存占满的完整流程整理出来,从free看内存状态、top/ps定位进程,到pmap深入单个进程、排查 slab 内核缓存,每一步都讲清楚命令背后的原理和判断依据。不管你是正在一线救火的运维,还是被内存问题缠身的后端开发,或者刚接触 Linux 想系统掌握排查思路的学生,这篇都能给你一套直接能用的行动方案。
1. 内存快满的第一反应:先别急着重启,先看数据
1.1 free 命令的正确打开方式:used 不是你以为的 used
收到内存告警,第一步永远是 SSH 上去敲free -h,而不是重启。不敲 free 直接重启,等于病人还没做检查就上手术台。万一这台机器内存里有大量缓存,重启反而把热数据全部清掉,业务冷启动变慢不说,问题的根源也永远找不到。
free -h的输出长这样:
total used free shared buff/cache available Mem: 31Gi 27Gi 505Mi 1.2Gi 3.2Gi 1.8Gi Swap: 4.0Gi 1.2Gi 2.8Gi字段有 total、used、free、shared、buff/cache、available。新手最容易踩的坑就是盯着 used 看,一看用了 27G 就慌了。实际上 Linux 内存管理有个核心设计:闲着也是闲着,不如把剩余物理内存拿去做文件缓存。所以 free 那一栏几乎总是很小,这不是内存不够,而是内核把空闲内存用在 page cache 上——文件读出来之后缓存在内存里,下次访问就不用再读磁盘。
判断内存到底紧不紧张,要看的是 available 这一列。available 是操作系统估算的、可以被回收再分配给新进程的可用内存,约等于 free 加上可回收的 cache。只要 available 还够用,业务没被 OOM Killer 杀掉,就说明系统还在正常运转。反过来,如果 available 长期低到只剩几百 MB,同时业务接口大量超时、日志里出现内存分配失败,那才是真正要动手的信号。
注意:
free看的是整机视角。如果服务器上跑了多个容器或者多个 Java 实例,整机 available 再高,某个容器也可能因为 cgroup 限额先被杀掉。所以整机看完,还得继续往下钻。
1.2 先判断是"真的不够"还是"看起来不够"
拿到free输出,我心里会快速过三个判断:
- 如果 used 高但 available 充足,说明内存大多被缓存吃了,属于"看起来不够",不用重启,也不用动缓存。
- 如果 available 也不够了,进入下一步,看谁在占用,但先别急着重启。
- 如果 swap 使用率也在涨,说明内存确实吃紧,内核已经开始往磁盘换页了,优先级要提到最高。
另外/proc/meminfo的信息比 free 细得多,排查时我会顺手看一眼,重点字段包括 MemAvailable、MemFree、Buffers、Cached、SReclaimable、SUnreclaim。Cached 是文件缓存,SReclaimable 是可回收的 slab 缓存(比如文件系统的 dentry 和 inode),SUnreclaim 是不可回收的 slab。这两个值在后面的内核内存排查里是核心线索。
还有一个容易被忽略的细节:/proc/pressure/memory这个文件记录内存压力指数,如果some或full的值持续走高,说明系统内存确实长期吃紧,而不是瞬时抖动。这个指标在排查"间歇性内存告警"时特别好用。把这些信息全部拉一遍,基本上就能判断告警是虚惊一场还是真出事了。
2. 进程级排查:找出谁在吃内存
2.1 top/htop 快速排序,别被 VIRT 骗了
top进去默认按 CPU 排序,要看内存得按 M 键,让进程按内存占用从高到低排列。这里需要理解几个概念:RES 是进程实际占用的物理内存,VIRT 是虚拟内存,SHR 是共享内存。真正应该看的是 RES。
很多新手看到 Java 进程 VIRT 有几十个 GB 会吓一跳,其实 VIRT 包含进程映射的全部地址空间,包括根本没访问过的共享库、预留但还没使用的堆区,基本不能作为内存占用的判断依据。
RES 也不是完美的。两个进程如果加载了同一块动态库,RES 里每个人各算各的,重复计数。所以几个进程的 RES 加起来超过物理内存总量,是常见现象,不代表内存泄漏。真实项目中我看到最多的情况是:一堆 Nginx worker 进程,每个 RES 看着不高,但加起来占掉好几个 G,这就是共享库重复统计的结果,不是故障。
2.2 ps 一条命令快速锁定 Top10
实际工作中我更喜欢用ps配合sort,一条命令列出内存占用 Top:
ps aux --sort=-%mem | head -20%mem是 RES 占总内存的百分比。这个命令的好处是输出格式稳定,适合写进脚本。配合watch可以持续刷新,观察排序变化:
watch -n 3 'ps aux --sort=-%mem | head -20'持续观察比单次快照信息量大得多。内存泄漏的进程会慢慢往上爬,临时飙高的进程会很快掉下去,这个动态变化本身就是判断依据。
还有一种情况:内存明明吃紧,但ps按内存排序后,发现没有一个进程特别突出,所有 RES 加起来也对不上整机的 used。这时候说明内存去向不在普通进程的 RES 里,得去查内核缓存和 slab,这是第 4 章要展开的内容。另外也可以看看/proc/PID/status里的 VmRSS 和 VmSwap,确认单个进程的真实占用和 swap 换出量。
2.3 smem 的神奇之处:多进程环境下看 PSS
top和ps给出的 RES 包含共享内存的重复计算,要精确核算每个进程"独享加按比例分摊共享"的真实消耗,可以用smem。这个工具很多服务器没默认装,在 Debian 系上apt install smem,Red Hat 系上yum install smem就行。
smem -rs PSS按 PSS 排序打印。PSS 的原理其实很简单:某块共享内存如果被 N 个进程同时使用,每个进程只摊 1/N。所以所有进程的 PSS 加起来,才和物理内存总量对得上。排查"到底谁偷了内存"时,PSS 比 RES 有说服力得多,特别是 Nginx 多 worker、Tomcat 多实例这类进程结构。
举个例子:一个 32G 内存的服务器,跑了 8 个 Java 实例和 16 个 Nginx worker。用ps看 RES,Java 实例每个占 3G,加起来 24G,好像快满了。但用smem看 PSS,Nginx worker 共享的那块动态库只算一份,Java 实例之间共享的 JVM 动态库也只算一份,真实占用可能只有 20G,还有富余。这种情况如果盲目加内存或者缩减实例,就是花冤枉钱。
3. 深入单个进程:从"谁在吃"到"为什么吃"
3.1 pmap 查看进程的完整内存地图
锁定了嫌疑进程之后,下一步是搞清楚它内部哪块内存最大,这时候用pmap:
pmap -x <PID>输出是这个进程的内存映射明细,每一行对应一块内存区域,包括代码段、堆、栈、共享库、mmap 映射等。重点看 RSS 列,尤其是不正常的巨大匿名内存段(anon),这类区域往往对应堆分配或者线程栈。
pmap输出末尾会汇总 total,列出各类映射的占比。看多了会有直觉:某个地址段动不动几十 GB,基本都是堆出了问题。如果匿名内存段异常大,说明进程的堆分配失控,下一步要去应用层找原因。如果大块内存都在文件映射段,可能是某个日志文件或者数据文件被 mmap 了,那就从配置上控制映射大小。
我习惯把pmap和/proc/PID/smaps配合使用,smaps 能按内存段给出更细的 Rss、Pss、Swap 数据。有时候还会用grep -A 10 "heap" /proc/PID/smaps精确看堆段的占用情况,比pmap的汇总更直观。
3.2 定位 Java 与 Python 应用的内存问题
Java 服务内存飙高,第一件事不是怀疑代码,而是先搞清楚是堆内还是堆外内存的问题。JVM 内存分堆内、堆外(直接内存)、元空间、线程栈四块,各管各的。
排查命令基本是这三条:
jmap -heap <PID> # 看堆的配置和当前用量 jstat -gcutil <PID> 1000 # 每秒打印一次GC情况 jmap -histo <PID> | head -30 # 看堆里对象的类型分布最典型的泄漏特征:Full GC 一直在跑,老年代占比居高不下,Young 区被压缩到极小,而且jstat多次采样显示堆占用只涨不降。这时候基本可以断定是对象没释放,拿着jmap -histo的类名去代码里找对应的缓存集合或者静态变量就行。
Python 进程则可以用py-spy抓分配栈:
py-spy dump --pid <PID>能直接打印当前 Python 进程的调用栈和大致内存归属,比瞎猜快得多。如果是 C/C++ 程序,valgrind --leak-check=full是经典方案,但会拖慢程序性能,一般用在测试环境复现,不适合直接上生产。
3.3 区分内存泄漏和临时性飙高:时间曲线法
拿到一批采样数据后,核心工作是画时间曲线。top或ps快照多打几次,间隔一两个小时甚至跨夜,把数据记录下来。如果内存占用持续上升不回落,是泄漏。如果涨到某个点稳定住了,或者随着流量涨落,说明是业务本身的合理占用,更可能是参数配错——比如缓存上限设置得太大、连接池默认值开满。
我遇到过一个特别迷惑的情况:服务重启后内存一两分钟就重新涨满,代码怎么看都像没问题。后来排查发现是启动加载时一次性把所有资源全读进内存,属于加载逻辑的问题,把同步加载改成延迟加载就解决了。所以"重启后是否很快复涨"这一步,能筛掉一大批伪泄漏。
还有一类容易被忽略的问题:线程数失控。每开一个线程默认要分配栈空间,虽然虚拟内存居多,但实际访问到的栈页也会占物理内存。ps -eLf | grep <PID> | wc -l数一下线程数,如果线程数量上千,光线程栈就能吃掉几个 G,这种情况先处理线程池配置,而不是纠结内存泄漏。
4. 内核缓存与不可回收内存:被误会的"罪魁祸首"
4.1 Page Cache、dentry 和 inode 是怎么堆积的
回到free的 buff/cache 部分,其实它是好几个东西合在一起的:文件内容的 page cache、目录项的 dentry cache、文件元数据的 inode cache。这些东西变大之后,在free里显示 buff/cache 很高,但系统内存紧张时会优先回收,一般不用管。
真正要警惕的,是/proc/meminfo里 Shmem 字段特别大的情况。Shmem 表示进程在使用 tmpfs 或共享内存,比如/dev/shm、某些中间件把临时数据放进共享内存。这部分既算缓存又不完全可回收,容易出现"free 看着够,业务却缺内存"的怪现象。
还有一种场景特别典型:某个目录下文件数量巨大,比如日志目录积累了上百万个小文件,dentry cache 会膨胀到几个 G。表现为free -h里 buff/cache 特别高,但业务读文件越来越慢,因为缓存都被海量文件元数据占满了。这种情况可以用sysctl vm.drop_caches=2强制回收 dentry 和 inode 缓存。
注意:不要动不动就
echo 3 > /proc/sys/vm/drop_caches。 page cache 是内核辛辛苦苦缓存的热数据,是性能的来源,不是事故。只有确认是 dentry/inode 这类元数据缓存异常膨胀,且磁盘性能可以接受的前提下才考虑手动回收。把缓存当敌人,是运维新手最典型的误解。
4.2 slab 内存的查看与回收边界
slab 是内核管理小对象缓存的机制,dentry、inode、网络连接结构、进程描述符都归它管。查看入口是/proc/slabinfo,但字段多到让人头皮发麻。更实际的做法是直接看SReclaimable和SUnreclaim两个值:前者可回收,后者不可回收。
如果 SUnreclaim 异常大——比如超过总内存的 10% 甚至更多——常见的坑是内核态驱动或者文件系统元数据出了问题。这种内存回收不掉,只能靠重启缓解,长期还要升级内核或者检查硬件驱动。
想看得更细可以用slabtop -s c,按缓存对象数量排序,能直接看到是哪个 slab cache 在膨胀。我在生产上见过kmalloc-64这类通用缓存异常增长的情况,最后定位到是某个内核模块频繁分配小对象但不释放。
4.3 不可回收的内核内存有哪些
除了 slab,还有一类更隐蔽的内存去向:页表、内核栈、vmalloc 区、以及内核代码段。这些在free里都算在 used 里,普通进程查不出来。如果整机内存明明没有多少进程在用,used 却居高不下,就要怀疑内核相关占用。
页表是记录进程地址映射的内存,进程越多、地址空间碎片越多,页表占的内存越大。一个进程光是页表就可能占几十 MB。内核栈是每个线程在内核态执行时的栈,默认 8KB 一个,线程多了也很可观。这些内存没有直接的回收开关,只能通过减少进程数、减少线程数来间接控制。
有一个比较实用的边界判断方法:把ps里所有进程的 PSS 加起来,对比free的 used,差额就是内核和其他不可归属的内存占用。如果这个差额长期异常大——比如几个 G——那基本可以断定问题出在内核层,而不是业务进程。
5. 实战排查清单与避坑技巧
5.1 高频问题速查表
把最常见的现象、原因、处理方式整理成一张表,排查时直接对照,能省不少时间:
| 现象 | 可能原因 | 优先排查手段 |
|---|---|---|
| available 长期不足,进程被 OOM 杀掉 | 某进程内存泄漏 | dmesg -T查 OOM 日志,pmap定位进程 |
| buff/cache 高,业务还能跑 | 正常缓存,不必处理 | 看free的 available 即可 |
| 重启后内存快速复涨 | 加载逻辑问题或泄漏 | 多次快照对比时间曲线 |
| 进程 RES 都不高但整机 used 高 | 内核 slab/页表占用 | slabtop、/proc/meminfo的 SUnreclaim |
| 多进程 RES 合计超物理内存 | 共享库重复统计 | smem按 PSS 核算 |
| 容器内内存告警,宿主机却正常 | cgroup 限额或统计口径不同 | 查memory.events里的 OOM 计数 |
OOM 日志是判断"谁是凶手"的第一手证据,必须会看:
dmesg -T | grep -i -E 'killed process|out of memory'输出里会点名哪个进程、占了多少内存、在什么时机被杀。如果 OOM 杀的是业务进程,同时还有别的进程占了大量内存,那嫌疑就非常明确了。cgroup 场景下还要看/sys/fs/cgroup/memory.events里的oom_kill计数,如果非零,说明容器确实被内核强制杀过进程。
5.2 我踩过的几个坑
第一个坑:报警脚本只看 used。生产环境里很多监控直接用 used/total 算使用率,结果缓存一上来就报警,人一慌就重启,业务跟着抖动。这个坑我自己也掉进去过,后来把监控指标改成 available,才算消停。建议所有内存告警都基于 available 计算,而不是裸的 used。
第二个坑:容器内存账和整机账对不上。容器场景下,free看到的是宿主机视角,容器内执行很容易被误导。排查容器内存,要看 cgroup 的统计:/sys/fs/cgroup/memory.current和memory.max。如果 current 已经顶到 max,但宿主机还有很多空闲内存,说明是容器限额的问题,加限额或者调业务内存占用才是正解。
第三个坑:忘了 swap。swap 如果已经大量换出,说明内存早在几天前就不够了,单看当前 free 的数字会被骗,因为一部分"脏数据"已经被挪到磁盘上了。排查时统一用swapon --show看 swap 用量,长期大于 0 且持续增长,基本可以判定内存一直紧张。
第四个坑:内存限制和实际需求不匹配。有时候根本不是泄漏,是业务设计时压根没算好内存预算。比如把全局缓存设置成不设上限、连接池默认值开满,这种问题不是"排查"出来的,是"读完配置"出来的。内存排查不能只盯代码,配置文件也要把所有参数过一遍,特别是-Xmx、缓存上限、线程池大小这三个高发点。
排查内存问题,最值钱的不是某条命令,而是"先记录、再定位、后处理"的顺序。遇到告警先别抢着重启,先把free、top快照、dmesg日志留一份,再做判断。我自己的习惯是准备一个两行的小脚本,把free -h、ps aux --sort=-%mem | head、dmesg最近一次 OOM 记录一起打上时间戳 dump 到一个日志文件,告警触发时先留下现场,再慢慢分析。另外,有条件的话给关键服务加上 cgroup 内存上限,宁可让进程在可控范围内被杀重启,也不要让它把整机拖垮。这套思路用顺手之后,内存告警从"半夜被叫起来救火"变成"白天坐下来看数据",差别还是挺大的。