Linux内存排查:free、available、buff/cache与OOM
2026/9/17 14:17:16 网站建设 项目流程

1. 先把"剩余内存"这个词拆开

1.1 free 那一列小得吓人,但不代表内存不够

线上机器一报警,很多人的第一反应就是敲free -h看 Linux 内存还剩多少。这个动作我做过不下几千次,但真正把输出里每一列搞清楚的人不多。最常见的误判是:看到free那一列只剩 200MB,慌得直接重启机器,其实那台机器离"内存不够"还差得远。

Linux 和另一种主流桌面系统在内存使用哲学上差别很大。后者倾向于让内存尽量空着,任务管理器里的"可用"数字接近真实空闲;Linux 反过来,只要有一块物理内存闲着,内核就恨不得拿它去做页缓存(page cache),把磁盘上的文件内容提前搬进内存,下次读同一个文件就不用再碰磁盘。这么做的直接后果是:机器跑得越久,free那一列越小,哪怕这台机器上只跑着两个很轻的服务。

用一个生活化的类比:你的仓库有 100 个货位,free表示此刻完全空着的货位,buff/cache表示堆着货但随时能挪走的货位。仓库管理员发现货位空着就顺手把常用的货提前铺开放着,走货更快。你进来一看过道全堆满了,以为仓库爆仓,其实只要有人来提新货,管理员三分钟就能清出一条道。

理解这一点,后面所有的数字就都好读了。

1.2 available 才是你要找的那个数

较新的内核(3.14 之后)在/proc/meminfo里加了一个字段叫MemAvailable,配套的新版procps-ngfree命令所属的软件包,3.3.10 以上)会在free输出里显示available列。这一列才是"在不触发 swap 的前提下,还能拿给新进程用的内存量"的估算值。

内核算这个数不是简单拿free加上buff/cache,而是走了一套有依据的估算逻辑,核心思路是:

available ≈ MemFree + (Active(file) + Inactive(file)) - min(该值/2, 低水位) + (SReclaimable) - min(该值/2, 低水位)

翻译成人话就是:只有文件相关的缓存页可回收的内核 slab才算数,匿名页(进程真正在用的堆栈内存)一律不算;同时还要扣掉一部分作为安全水位保护,防止把缓存全吃掉之后系统立刻陷入危险。低水位(wmark_low)可以去/proc/zoneinfo里翻,是内核给每个内存 zone 留的应急余量。

所以判断一台机器内存够不够,只看available这一列。free列可以低到几十 MB,只要available还有几 GB,这台机器就是健康的。

1.3 buff/cache 里的水分:有两类内存看着能回收,其实不能

新手最容易犯的第二个错,是把buff/cache整个当成"随时可以腾出来的内存"。大体上没错,但有两处水分必须扣掉。

第一处是Shmem。tmpfs(包括/dev/shm/run挂在 tmpfs 的部分)占用的内存会被算进Cached这一列里,看上去像是"文件缓存、可回收",但 tmpfs 压根没有对应的磁盘文件,除非系统配了 swap,否则这些页面没法写回磁盘,也就回收不了。我见过一个案例:有人把大量中间数据写到/dev/shm里图省事,结果buff/cache显示十几 GB 很漂亮,真到了内存紧张的时候内核一点都回收不动,直接被 OOM。判断方法很简单,看/proc/meminfo里的Shmem字段,这个数有多大,Cached里就有多大的水分。

第二处是正在回写的脏页。DirtyWriteback字段不为零的时候,对应的页面不能立刻丢弃,得先落盘。Dirty长期偏高说明写回不及时,这时候内存回收会卡在 IO 上,表现为系统整体变卡、si/so飙升,但available数字看着还行。这种"数字健康但体验很差"的情况,只盯available是看不出来的,得结合vmstat一起看。

2. 四条命令覆盖 90% 的日常场景

2.1 free -h:第一眼该看哪一列

日常排查,free -h是使用频率最高的。它的输出长这样:

$ free -h total used free shared buff/cache available Mem: 62Gi 18Gi 1.2Gi 412Mi 43Gi 43Gi Swap: 8.0Gi 256Mi 7.7Gi

各列含义和"能不能算进可用内存"的对照关系我整理成表格:

列名含义能否算进"可用"
total物理内存总量,通常比标称容量小一点分母,不参与判断
used已用内存,等于 total - free - buff/cache参考
free完全空闲的物理页能,但它只是可用量的下限
sharedtmpfs / 共享内存占用(新版已移到 buff/cache 里)基本不能,除非有 swap
buff/cache块设备缓冲 + 页缓存大部分能,但要扣掉 Shmem 和不可回收 slab
available内核估算的可用量以它为准

几个实用参数值得记住:

  • free -m/free -g:按 MB / GB 显示,看大数更直观。
  • free -s 2 -c 5:每 2 秒刷新一次,共 5 次。判断内存是在涨还是在跌,单次采样没意义,必须看趋势。
  • free -w:把bufferscache拆成两列单独显示(需要 procps-ng 3.3.12 以上),排查 IO 相关问题时有用。
  • free -t:最后多一行 total,把内存和 swap 加起来。

注意:available列在老旧系统上可能不存在。CentOS 6 那一代机器(内核 2.6.32)的free就没有这一列,得自己估算,方法见 3.1 节。

2.2 /proc/meminfo:所有内存数字的源头

freetopvmstat这些命令显示的内存数据,追根溯源全部来自/proc/meminfo。想搞清楚任何一个数字是怎么来的,直接cat /proc/meminfo | head -n 20,单位是 kB。

常用字段我挑出来列一下,都值得记:

字段说明
MemTotal可用物理内存总量,比标称小是因为内核镜像、硬件保留区、部分驱动先占了一部分
MemFree完全空闲的页
MemAvailable内核估算的可用量
Buffers块设备元数据缓存,通常不大
Cached文件页缓存,包含 Shmem
Shmemtmpfs 和共享内存,Cached里的水分所在
SReclaimable / SUnreclaim可回收 / 不可回收的内核 slab
Active(anon/file)、Inactive(anon/file)四个 LRU 链表的页数,available的计算基础
Dirty / Writeback待写回 / 正在写回的脏页
SwapTotal / SwapFree交换区总量和剩余
AnonPages匿名页,进程堆栈占的,基本不可回收
Mapped被 mmap 映射的文件页
PageTables页表本身占的内存,大内存机器上能到几个 GB
CommitLimit / Committed_ASovercommit 机制下的承诺上限和已承诺量

提取单个字段用awk最顺手,注意$2是数值、$3永远是单位kB

# 提取可用内存,单位从 kB 转成 GB awk '/^MemAvailable:/{printf "%.2f GB\n", $2/1024/1024}' /proc/meminfo

顺手提一个容易忽略的项:PageTables。每分配一页内存,内核都要为它维护一条页表项。跑大量小内存进程(比如几千个小容器、几万个线程)的机器上,页表本身的开销可能吃掉好几 GB。有些时候你发现所有进程的 RSS 加起来和used对不上,差的那部分就在这里。

2.3 top / ps:把内存定位到具体进程

知道还剩多少之后,下一个问题永远是"谁在吃"。top进去按M键按内存排序,是最快的路子。命令行一次性输出可以用ps

ps -eo pid,ppid,user,rss,vsz,pmem,comm --sort=-rss | head -n 11

这里必须讲清楚top/ps里几个内存列的区别,不然很容易误判:

  • VIRT / VSZ:虚拟内存大小,包含进程申请了但还没真正用到的地址空间,包含共享库映射。这个数字大得离谱很正常,Java 进程动不动几十 GB,不代表真占了这么多物理内存。
  • RES / RSS:常驻内存,进程当前真正在物理内存里的页。这是判断"吃了多少内存"的主要依据,但它有个坑:共享库、共享内存会被每个使用它的进程各算一遍。
  • SHR:RSS 里属于共享的部分。

所以你把所有进程的 RSS 加起来,结果远超物理内存总量,这不是出错,是共享页被重复计数了。想拿到相对准确的进程独占内存,得用smem工具(需要额外安装),它基于/proc/<pid>/smaps计算 PSS(按共享比例分摊),加总起来才接近真实占用。

单个进程想看细节,直接读它的 status 和 smaps 汇总:

grep -E 'VmRSS|RssAnon|RssFile|RssShmem|VmSwap' /proc/<pid>/status cat /proc/<pid>/smaps_rollup

RssAnon(匿名页)和RssFile(文件映射页)分开看,是判断"这个进程是真吃内存还是只是映射了一堆文件"的关键,后面 4.2 节会展开。

2.4 vmstat 和 slabtop:看趋势和内核态占用

单点采样只能说明"现在",判断内存压力要看趋势。vmstat是干这个的:

vmstat 1 10

重点看两列:si(从 swap 换入)和so(换出到 swap)。这两列持续非零,说明内存真的紧张了,内核已经在把匿名页往 swap 上倒腾。偶尔蹦出来一次不为零无所谓,那是内核在做平衡;连续十几个采样周期都是几百上千,就说明物理内存不足以支撑当前工作集。

free列在vmstat里也很直观,但注意vmstatbuffcache是分开的两列,加起来对应free输出里的buff/cache

另一个常被忽略的方向是内核自己吃掉的内存:

slabtop -o -s c

这条命令按对象大小排序显示内核 slab 缓存。dentryinode这两项通常最大,它们属于SReclaimable,内存紧张时能自动回收,不用管。但如果SUnreclaim那一列很大,说明有内核模块在漏内存,这种只能靠重启或者卸载模块解决,应用层怎么优化都没用。我就遇到过一次某监控 agent 的内核模块持续申请 socket 相关 slab 不释放,available一天掉一点,最后是靠slabtop对比两天快照定位到的。

3. 几个容易算错的指标和算法

3.1 老系统没有 MemAvailable,怎么自己估

内核 3.14 之前的系统没有MemAvailablefree命令即使升级了也只能显示+/- buffers/cache那两行(老式输出)。这种环境下的估算思路,是把"能回收的"加起来再扣掉安全水位:

awk ' /^MemTotal:/ {t=$2} /^MemFree:/ {f=$2} /^Buffers:/ {b=$2} /^Cached:/ {c=$2} /^Shmem:/ {s=$2} /^SReclaimable:/ {r=$2} END { # 可回收页缓存扣除 Shmem,再加上可回收 slab,最后留 5% 作安全水位 est = f + b + (c - s) + r - t*0.05; if (est < 0) est = 0; printf "估算可用: %.0f MB (%.1f%%)\n", est/1024, est*100/t; }' /proc/meminfo

三个关键点解释一下为什么要这么写。

一是Cached要减去Shmem。前面说过,tmpfs 占的页算在Cached里但回收不了,不减掉就会高估。

二是要加SReclaimable。dentry、inode 这些缓存内存紧张时内核会自动收缩,属于真正可用的部分。但要留意SReclaimable里也有回收不动的(比如某些驱动注册的),所以这里只是个上界估计。

三是扣掉 5% 的安全水位。内核不会让可用内存真的归零,每个内存 zone 都留了min/low/high三档水位。可以通过grep -A2 'low' /proc/zoneinfo看到具体数值,再乘上 zone 数量。没耐心算的话按总内存 5% 估一般够用,64GB 的机器留 3GB 左右,比较接近内核的默认行为。

3.2 swap 用了多少算安全

很多运维规范里写着"swap 使用率超过 50% 就告警",这条规则我建议改掉。swap 被用了一部分,本身是正常的,内核把不常访问的匿名页换出去,反而给页缓存腾了地方。真正要盯的是换入换出频率,也就是si/so

一个简单的判断口径:

  • SwapFree少,但si/so长期为 0:没问题,那些页只是躺在 swap 里没人碰。
  • SwapFree还很多,但si/so持续几百上千:有麻烦,说明工作集已经超过物理内存,系统在反复换页,磁盘 IO 会成为瓶颈。
  • 完全没有 swap,available又很低:一旦内存耗尽,唯一的出路就是触发 OOM Killer 杀进程,没有缓冲余地。

vm.swappiness这个参数控制内核有多积极地换出匿名页。默认 60 对数据库类应用偏激进,很多生产环境会调成 1 到 10,让内核尽量保留匿名页、优先回收文件缓存。但它只影响倾向,不改变总量,物理内存真的不够时它救不了你。

3.3 available 说够,为什么分配还是失败

这种情况我遇到过几次,都是被available这个估计值误导了。原因大概有这四类。

内存碎片。available是总量估计,不代表有一块连续的大内存可用。要申请大页(HugePage)或者给网卡、GPU 做 DMA 映射的时候,需要物理连续的内存块,碎片一多就分配失败,哪怕available显示还有几 GB。

overcommit 策略。/proc/sys/vm/overcommit_memory的值。设为 2 的时候内核严格按CommitLimit来审批,Committed_AS接近CommitLimit就会拒绝malloc,而这个拒绝跟available完全无关。数据库和 Redis 场景下经常要调这个参数,调之前先算清楚CommitLimit = swaptotal + ram * overcommit_ratio / 100

cgroup 限制。容器里的进程看到的available是宿主机的,但它实际能用的受 cgroup 内存上限约束。这时候free的数字完全没有参考价值,得去读 cgroup 的计数文件。

进程级限制。ulimit -v设了虚拟内存上限,或者RLIMIT_AS被容器运行时限制了。这时候malloc失败返回 NULL,程序报错,但系统层面一切正常。

提示:排查"分配失败但内存看着够"的问题,按这个顺序查——先看ulimit -v,再看 cgroup 限制,再看Committed_AS / CommitLimit,最后才怀疑碎片。前三个都能一眼看出来,碎片问题最麻烦。

4. 实战:三类"内存不够"的现场怎么查

4.1 进程突然消失:OOM Killer 复盘

最典型的内存事故是某个进程毫无征兆地消失,日志里只有一行莫名其妙的断开。八成是被 OOM Killer 杀了。查询路径:

dmesg -T | grep -i -E 'oom|killed process' # systemd 系统也可以看内核日志 journalctl -k --since "1 hour ago" | grep -i oom

输出里会明确写出被杀进程的 PID、名字、占用的 RSS,以及当时的内存快照(total pagecachefreeslab各多少),还会列出所有候选进程的oom_score。这份现场记录非常宝贵,能直接告诉你事发瞬间是谁把内存吃满的。

谁会被选中,取决于oom_score,而这个分数受/proc/<pid>/oom_score_adj影响(旧内核里叫oom_adj)。取值范围 -1000 到 1000,越小的越不容易被杀。生产环境里给核心进程调低这个值是个常见做法:

echo -500 > /proc/$(pidof mysqld)/oom_score_adj

但我不建议一上来就靠这个保命。把关键进程保护起来,代价是 OOM 时内核会去杀别的进程,可能连 sshd 或者监控 agent 都被干掉,机器直接失联。正确思路是先把内存问题的根因找到。

还有一个容易忽略的点:在容器里,判断"谁被杀了"要看 cgroup 层面。容器内存超限时,内核杀的是该 cgroup 里oom_score最高的进程,dmesg里同样会有记录,但看到的可能是宿主机视角的 PID,跟容器内的 PID 对不上。排查时要结合容器运行时的事件日志一起看。

4.2 内存只涨不跌:这是泄漏还是缓存

这是被问得最多的问题:某个进程的 RSS 每天涨一点,三个月后要重启一次,到底是代码泄漏还是正常的缓存行为。

判断方法其实很明确:RssAnonRssFile的比例变化

# 采样两次,间隔几小时,对比匿名页的增长 awk '/RssAnon|RssFile|RssShmem|VmRSS/{print}' /proc/<pid>/status

RssFile增长基本可以放心,那是文件页缓存被算进了进程的 RSS,内存紧张时会自动丢。真正要警惕的是RssAnon持续单调增长——匿名页只增不减,说明堆上分配了东西没释放,这就是泄漏的特征。

进一步定位,用pmap看是哪一段地址空间在涨:

pmap -x <pid> | sort -k3 -n -r | head -n 20

输出里[heap]段持续变大是最常见的泄漏信号。配合smaps能看得更细:

awk '/^[0-9a-f]/{addr=$1} /^Rss:/{if($2>10240) print addr, $2" kB"}' /proc/<pid>/smaps | sort -k2 -n -r | head

这样能揪出超过 10MB 的具体内存段。

对于 JVM 系的应用,还要额外区分堆内和堆外。堆有上限,看 GC 日志就知道是不是满;真正容易失控的是堆外——DirectByteBufferMetaspace、JIT 编译缓存、线程栈。堆外内存泄漏的特征是:堆用得不多,但进程 RSS 一直涨,-XX:MaxDirectMemorySize没设置的话默认等于堆大小,很容易超。排查用Native Memory Tracking

java -XX:NativeMemoryTracking=summary -jar app.jar jcmd <pid> VM.native_memory summary

Redis 也有类似的坑。fork做持久化的时候会触发写时复制(COW),如果这期间写入量大,内存可能翻倍。所以maxmemory不能设到接近物理内存上限,得留出至少一半余量,或者干脆关掉自动重写、改用其他持久化策略。

4.3 容器里的读数为啥全是错的

在容器里敲free -h,看到的是宿主机的内存数据,不是容器的。原因是/proc/meminfo属于 procfs,早期并没有做命名空间隔离,容器共享宿主机的这份数据。

这就导致一个很尴尬的局面:容器内存限制 1GB,free显示宿主机还有 100GB 可用,应用以为自己很宽裕,放心大胆地申请内存,然后被 cgroup 一记闷棍打死。

正确的读法是看 cgroup 接口。cgroup v2 的路径在/sys/fs/cgroup/下:

cat /sys/fs/cgroup/memory.current # 当前使用量 cat /sys/fs/cgroup/memory.max # 上限,无限制时显示 max cat /sys/fs/cgroup/memory.stat # 细分:anon / file / slab 等

cgroup v1 则是/sys/fs/cgroup/memory/memory.usage_in_bytesmemory.limit_in_bytes

memory.stat里的anonfile分得很清楚,比/proc/meminfo更适合做容量判断。另外file那一项在 v2 里包含 page cache,容器内存"看起来超标"很多时候就是它,可以通过写入memory.reclaim主动回收试试,或者调整memory.high做软限制。

如果确实需要在容器里给出接近真实的free输出,可以考虑挂载专门做 procfs 虚拟化的方案,把meminfo按 cgroup 限制换算后再呈现,这样应用不需要改代码就能拿到合理读数。这是运维层面的绕行方案,不是内核行为。

5. 常见问题速查与避坑清单

5.1 问题速查表

把日常遇到的情况和对应处理整理成表,出问题直接对号入座:

现象优先查什么常见原因处理方向
free 很低但系统正常available页缓存占了空闲内存不用管,这是设计如此
available 持续下降vmstat 1的 si/so工作集超过物理内存扩容或优化内存占用
si/so 持续非零swap使用趋势已进入换页状态查进程 RSS 排行,考虑扩容
进程被无声杀掉dmesg -T | grep -i oomOOM Killer分析内存增长来源
RSS 只涨不跌RssAnon变化趋势堆内存泄漏pmap/ NMT 定位代码
available 够但 malloc 失败ulimit -v、cgroup 限制进程级或 cgroup 配额调整限制或 overcommit
buff/cache 高但回收不动ShmemSUnreclaimtmpfs 占用、内核泄漏清理 tmpfs 或重启
容器内 free 数字离谱memory.currentprocfs 未隔离读 cgroup 接口

5.2 我踩过的几个坑

别随手清缓存。echo 3 > /proc/sys/vm/drop_caches这条命令网上流传很广,有人说"内存不够就清一下"。我劝你别在生产环境用。它会一次性丢掉所有页缓存,接下来一段时间所有文件读取都要回磁盘,IO 直接打满,业务响应时间会肉眼可见地恶化。这个操作只适合做性能测试时构造一致的初始条件,跑完就恢复。

别只看峰值告警。内存监控只看某个瞬时峰值意义不大,available从 20GB 掉到 19GB 然后反弹,属于正常波动;从 20GB 花三天匀速掉到 2GB,才是真问题。我现在的做法是用sar -r或者采集指标落库,看一周的斜率。

别把 RSS 加起来当总用量。前面提过,共享库和共享内存会被重复计算,几十个进程加起来超出物理内存是家常便饭。真要算进程维度,上smem看 PSS。

tmpfs 的容量要单独管。/dev/shm默认大小通常是物理内存的一半,很多应用(数据库的共享内存段、某些语言的并行运行时)会往里写东西,一不留神就吃掉几 GB。可以用df -h /dev/shm随时看占用。

最后分享一个我在用的监控脚本,逻辑很简单:定期采样MemAvailable,低于阈值就把内存占用前 10 的进程打出来。放在 crontab 里跑,事后复盘时日志特别有用。

#!/bin/bash # mem_check.sh —— 可用内存低于阈值时记录 TOP 进程 THRESHOLD=15 # 可用内存百分比阈值 read -r TOTAL AVAIL < <(awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print t, a}' /proc/meminfo) PCT=$(awk -v a="$AVAIL" -v t="$TOTAL" 'BEGIN{ if(t==0){print 0} else {printf "%.1f", a*100/t} }') echo "$(date '+%F %T') total=${TOTAL}kB avail=${AVAIL}kB avail_pct=${PCT}%" if awk -v p="$PCT" -v th="$THRESHOLD" 'BEGIN{exit !(p < th)}'; then echo "[WARN] available below ${THRESHOLD}%" ps -eo pid,ppid,user,rss,pmem,comm --sort=-rss | head -n 11 fi

回到最开始那个问题。我现在判断一台 Linux 机器内存够不够,逻辑已经简化成三步:先free -havailable,再vmstat 1 5si/so,然后ps --sort=-rss看谁在吃。三步走完,二十秒内基本能得出结论,剩下的就是照着上面几个坑逐条排除了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询