Linux 内存去哪儿了:free 的 used 为什么不能信
一台"内存吃满"的机器
监控报警:某台 8G 的服务器内存使用率 96%,持续半小时。登上去看:
$ free -h
total used free shared buff/cache available
Mem: 7.6Gi 6.9Gi 210Mi 12Mi 512Mi 330Mi
Swap: 2.0Gi 1.8Gi 200Mi
used 6.9G,available 只剩 330M,swap 也快用完了。但 ps aux --sort=-rss | head 排下来,最大的进程才 800M,前十个加起来不到 3G。剩下 4G 去哪了?
这就是 free 最容易误导人的地方:used 里面混着大量可以回收的东西,而 available 才是你真正能用的内存。反过来,有时候 used 看着不高,进程却一个个被 OOM 杀掉——因为限制不在整机,在 cgroup。
这篇把 Linux 内存的账算清楚:哪些能回收、哪些不能、cgroup 里的数字怎么读、以及内存问题的排查顺序。
一、free 的输出到底怎么读
$ free -m
total used free shared buff/cache available
Mem: 7807 6801 205 12 800 331
四个关键字段的关系:
used = total - free - buff/cache
available ≈ free + 大部分 buff/cache + 可回收的 slab
free:完全没被使用的内存。这个数字小是正常的——Linux 会主动用空闲内存做缓存,空闲内存等于浪费;buff/cache:页缓存(page cache)+ 块设备缓存 + 可回收 slab。这部分大部分可以随时回收给应用;available:内核估算的"在不触发 swap 的情况下,还能给新进程用多少"。这是唯一该看的内存余量指标;used:已经分配出去的,但其中包含共享内存、tmpfs、以及暂时不可回收的部分。
所以上面那台机器真正的问题是:available 只有 330M,说明可回收的部分已经很少了,不是 buff/cache 撑的。
如果 used 高但 available 也高(比如 used 6G、available 3G),那说明大部分是可回收缓存,不必紧张。
二、内存去哪了:五个去处
① 匿名页(anonymous pages)—— 应用真正占用的
进程的堆、栈、malloc 出来的内存。这部分不能回收,只能 swap 出去。ps 里的 RSS 就是它的一部分(不含共享页)。
② 页缓存(page cache)—— 可回收
读过的文件、mmap 的文件都在这。它让第二次读同一个文件不用碰磁盘。可用内存紧张时内核会回收它,所以它不影响 available。
判断方法:/proc/meminfo 里的 Cached,或者
$ grep -E "^(Cached|Buffers|MemAvailable|AnonPages)" /proc/meminfo
MemAvailable: 339620 kB
Buffers: 20132 kB
Cached: 731004 kB
AnonPages: 6233488 kB
这里 AnonPages 6.2G、Cached 只有 700M —— 结论很清楚:内存全在应用手里,没有缓存可回收。这就是为什么会 OOM 而不是"把缓存让出来"。
③ slab —— 内核对象缓存,分可回收与不可回收
dentry / inode 缓存(可回收)、socket 缓冲区、task_struct 等(不可回收)。看:
$ slabtop -o -s c | head -5
$ grep -E "SReclaimable|SUnreclaim" /proc/meminfo
SUnreclaim 持续增长且不回落,通常是内核对象泄漏(比如文件描述符没关、连接没释放),这类问题重启才掉,得从代码查。
④ 共享内存 / tmpfs
shared 那列。/dev/shm、/tmp 挂成 tmpfs 时,写进去的文件占的是内存不是磁盘。一个常见事故:程序往 /tmp 写了几个 G 的临时文件,df -h /tmp 看着还有空间(tmpfs 上限是内存的一半),结果整机 OOM。
⑤ swap
swap 用满不等于故障,但它说明匿名页已经溢出到磁盘。Swap 行如果 used 持续增长,通常意味着匿名页总量超过了物理内存,或者有内存泄漏在慢慢吃掉。
三、RSS 加起来对不上 used,是正常的
常见困惑:所有进程 RSS 加起来 7G,但 free 显示 used 只有 5G;或者反过来。原因有四:
- 共享库被重复计算。每个链接 libc 的进程都会在 RSS 里算上 libc 的页,实际只有一份。
Pss才是按共享比例分摊后的值(/proc/<pid>/smaps_rollup); - 页缓存不算进进程 RSS。文件读进内存后归在
Cached,不归任何进程; - slab、内核栈、页表本身都不在进程 RSS 里;
- tmpfs 里的文件看起来在"文件系统"里,实际占内存。
所以"进程 RSS 总和 vs used"永远对不上,别试图凑平。要精确核对,用:
$ awk '/^(AnonPages|Cached|SReclaimable|SUnreclaim|Slab|PageTables|KernelStack|Shmem):/{print}' /proc/meminfo
把这几项加起来,才是真正的去处。
四、cgroup:真正杀你的那个限制
很多人遇到"内存明明还有,进程却被 Killed",答案在 cgroup。
容器(Docker/K8s)通过 cgroup 给进程组设内存上限。此时整机内存充足也没用,撞的是组的上限。cgroup v2 看这几个文件:
$ cat /sys/fs/cgroup/memory.current # 当前用量
$ cat /sys/fs/cgroup/memory.max # 上限(max 表示不限)
$ cat /sys/fs/cgroup/memory.stat # 分项:anon / file / slab / kernel
$ cat /sys/fs/cgroup/memory.events # 关键!oom 计数
memory.events 里 oom_kill 计数大于 0,就说明这个 cgroup 里确实发生过 OOM 杀进程。想确认是"整机 OOM"还是"cgroup OOM",看 dmesg -T | grep -i "oom":
Memory cgroup out of memory: Killed process 12345 (java) total-vm:...
注意 cgroup v2 里 memory.current 把 page cache 也算进去了。一个只读大量文件的容器,memory.current 会接近上限,但这不一定会 OOM——内核会先回收 file 页。如果它持续顶到上限并且 memory.events 里 high / max 计数一直涨,那才是真压力。
还有个坑:Java 进程看不到 cgroup 限制(JDK 8 早期版本、或者没启用容器感知)。-Xmx 按整机内存算,容器限额 2G 它敢申请 8G,于是被 OOM kill 得毫无征兆。JDK 10+ 默认开启 UseContainerSupport,老版本要手动加。
五、OOM killer 为什么杀了"看起来最无辜"的进程
内核按 /proc/<pid>/oom_score 打分,分数高的先杀。影响分数的因素:占用内存大小、进程运行时长、oom_score_adj 设置。
$ cat /proc/$(pgrep -f myservice)/oom_score
$ cat /proc/$(pgrep -f myservice)/oom_score_adj
oom_score_adj 范围 -1000 到 1000。设成 -1000 表示"永不杀我"(慎用,可能让系统进入死锁),设成 1000 表示"优先杀我"。
生产上的常见做法:
- 关键守护进程(sshd、监控 agent)设
oom_score_adj = -500左右,避免 OOM 后连不上去排查; - 批处理、可重启的任务设正数,让它优先被牺牲。
但不要指望调分数解决问题,它只决定"杀谁",不解决"内存为什么不够"。
六、排查顺序:从大到小,从粗到细
我遇到内存问题时的固定顺序:
第一步:确认是不是真压力。
free -h # 看 available,不看 used
cat /proc/pressure/memory # PSI:some/full 的 avg10 持续 > 5 才算有压力
PSI(pressure stall information)比使用率准得多:full 高说明有进程真的卡在等内存上。内核 4.20+ 支持。
第二步:分清整机还是 cgroup。
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.events
dmesg -T | grep -i oom | tail -5
第三步:看内存构成。
grep -E "^(AnonPages|Cached|SReclaimable|SUnreclaim|Slab|Shmem):" /proc/meminfo
AnonPages 大 → 应用占的,去查进程;SUnreclaim 大且持续增长 → 内核对象泄漏;Shmem 大 → 查 tmpfs。
第四步:找具体进程。
ps aux --sort=-rss | head -10
# 更准:按 PSS 排序
for p in /proc/[0-9]*; do awk '/^Pss:/{s+=$2} END{if(s>0) print s, "'"$p"'"}' $p/smaps_rollup 2>/dev/null; done | sort -rn | head -10
第五步:判断是缓慢泄漏还是突发。
- 缓慢泄漏:观察
AnonPages是否单调递增,配合pmap -x <pid>或堆 dump; - 突发:查最近的流量、批处理任务、是否有大文件读入内存。
七、几个反直觉的结论
used 高不等于有问题。 一台跑了半年的数据库,used 90%、available 低是正常的——它就该占着内存别让出去。判断标准是 PSI 和有没有 OOM。
buff/cache 高不需要"清理"。 网上那些 echo 3 > /proc/sys/vm/drop_caches 的"优化",除了让接下来的读变慢,没有任何好处。内核比你更清楚该回收什么。
swap 不是洪水猛兽。 少量 swap 反而能让冷页让出内存给热数据。真正的问题是频繁 swap in/out(vmstat 1 里 si/so 持续非零),那叫 thrashing,比 OOM 还糟——系统活着但什么都不干。
内存"泄漏"未必在应用。 见过好几次是文件描述符没关导致 dentry 缓存涨、连接没释放导致 socket 缓冲区堆积,应用自己的堆很干净。查的时候别只看堆。
小结
Linux 内存的账,核心就三句话:
- 看
available和 PSI,不看used; - 分清楚匿名页(不可回收)和页缓存(可回收)——前者才是风险;
- 确认限制在哪一层:整机、cgroup、还是 JVM 自己的堆上限。
把这三句记住,绝大多数"内存吃满"的报警都能在五分钟内定位到具体哪一层,而不是上来就重启。
- 点赞
- 收藏
- 关注作者
评论(0)