2021 年,一次"我盯着 free -h 那个用了 15G 的内存,熬了一整夜抓所谓的'内存泄漏',最后发现【根本没有泄漏】"的事故,把我对"内存被用掉了"这件事的理解,从头到尾翻新了一遍。那台服务器 16G 内存,上面跑着一个 Java 服务。某天我例行巡检,free -h 一看,used 那一列写着 15G——16G 的机器,用了 15G!我心里"咯噔"一下:内存快满了,这服务器随时要崩。我第一反应:Java 服务内存泄漏了。我立刻进入战备状态,导堆、看 GC 日志、分析对象引用……一整夜过去,Java 进程的堆,稳得像块石头,jmap 看到的堆用量才 2G 多,健康得很。可 free -h 那个 15G,纹丝不动地刺在那里。我开始怀疑是不是别的进程在吃,ps 把所有进程的内存加起来——所有进程的内存总和,也就 4G 出头。4G。可 free 说用了 15G。中间那 10 个多 G,被谁吃了?机器上每一个进程,我都查过了,它们加起来才 4G。free 口口声声说 15G 被 used 了——可这个系统里,我【找不到任何一个进程】对得上这笔账。这 10 个 G,既不在任何进程名下,又确确实实显示为"已用"——它到底【是什么】?如果它真被"用掉"了,为什么没有任何一个进程认领它?如果它没被"用掉",free 为什么把它算进 used?这件事逼着我把 free 的每一列、buff/cache 是什么、available 和 free 的天壤之别,彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,16G 内存,跑一个 Java 服务
事故现象:
- free -h 看到 used 15G,16G 的机器只剩 1G
- ★ 以为内存泄漏,熬夜导堆分析 —— Java 堆才 2G,很健康
- ★ 所有进程内存加起来才 4G,那 10 多个 G 谁吃了?
现场排查:
# 1. ★ free -h 看到的(CentOS 7 的输出)
$ free -h
total used free shared buff/cache available
Mem: 16G 15G 320M 80M 11G 13G
# ^^^ ^^^ ^^^
# ★ used 吓人 ★ 这两列才是关键
# 2. ★ 把所有进程的物理内存(RSS)加起来
$ ps aux --sort=-rss | awk 'NR>1{s+=$6}END{print s/1024/1024" GB"}'
4.2 GB # ★★ 所有进程才 4.2G!
# 3. ★ 那 buff/cache 那 11G 是什么
$ cat /proc/meminfo | grep -E 'Cached|Buffers'
Buffers: 260000 kB
Cached: 11200000 kB # ★★ 11G 在这:页缓存
# 4. ★ 这 11G,能不能被新程序拿去用
$ free -h | awk 'NR==2{print "available: "$7}'
available: 13G # ★★ 实际可用 13G!
根因(后来想清楚的):
1. ★ free 里的 used,在老版本/老观念里是个【极易
误导】的数字。它把 buff/cache 算成了"已用"。
2. ★ 那 11G buff/cache,是【页缓存(page cache)】:
内核把读过的磁盘文件内容,顺手缓存在空闲内存里,
下次读同样的文件就不碰磁盘,直接命中内存。
3. ★ 关键:页缓存占的内存,是【可回收】的。一旦
有进程真要内存,内核会【立刻把页缓存丢掉】,
把这部分内存腾给进程 —— 它不是"被占死"的。
4. ★ 所以"内存够不够用",根本【不该看 used / free】,
要看 ★ available —— 它 = free + 可回收的缓存,
才是"还能给新程序用多少"的真相。本文 available
是 13G,内存【健康得很】。
5. ★ 我熬夜抓的"泄漏",是个根本不存在的幽灵。那
11G 不是被泄漏掉的,是内核【主动拿空闲内存做了
缓存】—— 这是内核在帮你加速,是好事,不是病。
6. 真相:内存被 buff/cache "占着",和内存被
"用掉了",是两件完全不同的事。空闲的内存不
拿来做缓存,才是浪费。
不是内存泄漏,是我把"内核拿空闲内存做的缓存",
错看成了"被进程吃掉、回不来的内存"。
修复 1:free -h 每一列,到底是什么
# === ★ 先把 free 的每一列,逐个钉清楚 ===
# === ★ free -h 的输出(CentOS 7+ / 现代内核)===
$ free -h
# total used free shared buff/cache available
# Mem: 16G 15G 320M 80M 11G 13G
# Swap: 2.0G 0B 2.0G
# === ★ total:物理内存总量 ===
# 这台机器一共有多少内存。16G。没有歧义。
# === ★ free:★ "完全没被碰过"的内存 —— 易误解 ===
# ★ free 不是"可用内存"!它是"此刻【一点用途都没
# 派上】、纯粹空着的"内存。
# ★ 一台健康、运行已久的 Linux,free 通常都【很小】
# (本文才 320M)—— 这【不是】问题。因为内核
# 会把暂时没人用的内存,统统拿去做缓存了。
# ★ ★ free 很小,绝不等于"内存不够"。
# === ★ used:★ 最容易吓人、最该警惕的一列 ===
# ★ 不同版本的 free,used 的算法不一样,历史上它
# 常常【把一部分缓存也算进去】,导致这个数虚高。
# ★ 看到 used 很大,★ 先别慌 —— 它不等于"进程
# 真正吃掉的内存"。
# === ★ buff/cache:内核拿空闲内存做的缓存 ===
# ★ 这部分内存,内核用它缓存了磁盘文件、文件系统
# 元数据。它【占着】内存,但【随时能还】。
# ★ 下一节专门讲它。
# === ★ shared:多个进程共享的内存(tmpfs 等)===
# 一般不大,先不深究。
# === ★★ available:★ 唯一该看的"还能用多少" ===
# ★ available = 当前 free 的 + buff/cache 里【可回收】
# 的那部分。它回答的是那个你真正关心的问题:
# "现在再起一个程序,大概还有多少内存能给它?"
# ★ ★ 判断内存够不够,只看 available 这一列。
# === ★ 一句话记住该看哪 ===
# ★ free 列:别看,它小是正常的。
# ★ used 列:别慌,它大可能是缓存虚高。
# ★ ★ available 列:只看它。它才是真相。
# === 认知 ===
# ★ free -h 六列:total 总量;free 是"完全没被碰过"
# 的内存(健康机器它本就很小,不代表内存不够);
# used 历史上常把缓存算进去,虚高吓人别慌;
# buff/cache 是内核拿空闲内存做的可回收缓存;
# ★ available 才是唯一该看的"还能给新程序用多少"。
# 判断内存够不够,只认 available 这一列。
修复 2:buff/cache——内核拿空闲内存做的缓存,是好事
# === ★ 把那"凭空消失的 10 多个 G",讲个明白 ===
# === ★ 页缓存(page cache)是什么 ===
# ★ 你的程序读一个磁盘文件,磁盘很慢。内核很聪明:
# 它把读过的文件内容,【顺手存一份在内存里】。
# 下次再读同一个文件,直接从内存命中,不碰磁盘。
# ★ 这份"内存里的文件副本",就是页缓存。free 里
# buff/cache 那一大坨,绝大部分就是它。
# === ★ 关键认知:内核会用掉【几乎所有】空闲内存做缓存 ===
# ★ 内核的哲学是:★ "空闲的内存 = 被浪费的内存"。
# 一段内存,你不用它,它也只是空着、产生不了任何
# 价值。内核索性把暂时没进程用的内存,【全部】
# 拿去做页缓存,让磁盘 IO 快起来。
# ★ 所以一台跑了很久的 Linux,buff/cache 很大、
# free 很小,是【绝对正常、且健康】的表现 ——
# 说明内核在尽职尽责地用内存帮你加速。
# === ★ 最关键的一点:缓存内存是"可回收"的 ===
# ★ buff/cache 占的内存,和进程吃掉的内存,有本质
# 区别:★ 它【随时可以被收回】。
# ★ 一旦有进程真的要内存、而 free 不够了,内核会
# 【立刻、自动】把页缓存丢掉一部分(那只是文件
# 副本,丢了大不了下次重新从磁盘读),把腾出来的
# 内存交给进程。这个过程,你完全感知不到。
# ★ 所以页缓存【不会】把进程"饿死"。它像一个
# "临时占着工位、但你一来就立刻让座"的人。
# === ★ 亲手验证:缓存能被瞬间释放 ===
$ free -h | awk 'NR==2{print "回收前 buff/cache: "$6}'
# 回收前 buff/cache: 11G
$ sync && echo 3 > /proc/sys/vm/drop_caches # ★ 手动丢弃所有缓存
$ free -h | awk 'NR==2{print "回收后 buff/cache: "$6}'
# 回收后 buff/cache: 300M # ★★ 11G 缓存瞬间还回来了
# ★ 这证明:那 11G 从来没"丢",它一直是【可还的】。
# ★ ★ 注意:drop_caches 仅用于验证 / 排查,生产环境
# 【不要】常做 —— 丢了缓存,磁盘 IO 会暂时变慢。
# === ★ 所以本文那"消失的 10 个 G",真相是 ===
# ★ 它没消失,也没泄漏。它是内核拿"反正也空着"的
# 内存,做成了页缓存。它不在任何进程名下,因为它
# 【本就不属于任何进程】,它属于内核的缓存层。
# ★ 我那一夜,是在追一个内核的"功能",当成了"故障"。
# === 认知 ===
# ★ buff/cache 主要是页缓存:内核把读过的磁盘文件
# 顺手缓存在空闲内存里加速 IO。内核哲学是"空闲
# 内存=浪费",所以它会用掉几乎所有空闲内存做缓存
# —— 跑久的 Linux buff/cache 大、free 小是健康表现。
# ★ 关键:缓存内存可回收,进程一要内存内核立刻丢
# 缓存让座,它不会饿死任何进程。这是加速,不是病。
修复 3:available——唯一回答"内存够不够"的列
# === ★ 把"该看哪个数"这件事,一次说死 ===
# === ★ 错误的判断:16G - used = 剩余 ===
# ★ 本文我栽的根:我用 "total - used" 算剩余 ——
# 16G - 15G = 1G,我就以为"只剩 1G,要爆了"。
# ★ 这个算法【是错的】。因为 used 里混着可回收的
# 缓存,这 15G 里有 11G 是随时能还的缓存。
# ★ "total - used" 算出的"剩余",严重低估了真实
# 可用量,会把健康的机器误判成濒死。
# === ★ 正确的判断:只看 available ===
$ free -h
# total used free shared buff/cache available
# Mem: 16G 15G 320M 80M 11G 13G
# ^^^
# ★ available = 13G。它的含义是:"如果现在启动一个
# 新程序,在【不动用 swap、不发生卡顿】的前提下,
# 内核大约能立刻给它 13G。"
# ★ 这 13G 怎么来的:320M 的 free + buff/cache 里
# 大约 12.7G 可回收的部分。
# ★ ★ 13G 可用 / 16G 总量 —— 这台机器内存【非常健康】。
# === ★ 为什么是 available 而不是 free + buff/cache ===
# ★ buff/cache 里并非 100% 都能回收 —— 有一小部分
# (如脏页、tmpfs、被锁定的页)收不回来。
# ★ available 是内核【自己算好】的"扣除了收不回的
# 部分之后,真正能拿出来的量"。所以直接信它,
# 别自己拿 free + buff/cache 去估。
# === ★ 老系统没有 available 列怎么办 ===
# ★ 很老的 free(procps 3.3.10 以前)没有 available
# 列,只有一行 "-/+ buffers/cache"。看那一行的
# free 部分。或直接读 /proc/meminfo:
$ grep MemAvailable /proc/meminfo
MemAvailable: 13600000 kB # ★ 这就是 available 的来源
# === ★ 一条简单的健康判断线 ===
# ★ available / total 还有 20% 以上 -> 内存健康。
# ★ available 持续走低、逼近 0 -> 才是真要担心,
# 这时再去查是哪个进程在涨(见修复 4)。
# ★ ★ used 多大、free 多小,都【不是】判断依据。
# === 认知 ===
# ★ 判断内存够不够,★ 绝不能用 total-used 算剩余
# (used 混着可回收缓存,会把健康机器误判成濒死)。
# 只看 available 这一列 —— 它是内核算好的、扣除了
# 收不回部分之后真正能立刻给新程序用的量。老系统
# 读 /proc/meminfo 的 MemAvailable。available 占
# total 20% 以上即健康,持续逼近 0 才需担心。
修复 4:真要排查内存,该看进程的什么
# === ★ 如果 available 真的低了,这样查进程 ===
# === ★ 第一步:按物理内存排序,揪出吃内存大户 ===
$ ps aux --sort=-rss | head
# USER PID %CPU %MEM VSZ RSS ... COMMAND
# ★ 看 RSS 那一列,从大到小。最上面的就是嫌疑人。
# === ★ 关键区分:VSZ 和 RSS 是两回事 ===
# ★ VSZ(虚拟内存大小):进程【申请 / 映射】了多大
# 的虚拟地址空间。它包含了很多【还没真正用到、
# 没占物理内存】的部分。VSZ 经常大得吓人,★ 它
# 【不是】进程真实占的内存,排查时基本可以忽略。
# ★ ★ RSS(常驻内存):进程【真正占用的物理内存】。
# 这才是它实打实吃掉的、要算账的内存。
# ★ 排查内存,★ 只看 RSS,别被 VSZ 吓到。
# === ★ 第二步:盯一个进程,看它的 RSS 是否持续涨 ===
# ★ "内存泄漏"的真正定义:一个进程的 RSS,【随时间
# 持续单调上涨,只涨不降】。
$ while true; do
ps -o rss= -p 12345 | awk '{print strftime("%H:%M:%S"),$1/1024" MB"}'
sleep 60
done
# ★ 连续观察:RSS 一直涨不回头 -> 真泄漏;
# 涨涨跌跌、稳定在一个范围 -> 正常,不是泄漏。
# ★ ★ 单看一个时刻的 RSS 大,不能叫泄漏 —— 泄漏是
# "趋势",必须看一段时间。
# === ★ 第三步:看一个进程内存的细致构成 ===
$ cat /proc/12345/status | grep -E 'VmRSS|VmSwap'
# VmRSS: 2100000 kB # 物理内存
# VmSwap: 50000 kB # 被换到 swap 的部分
$ pmap -x 12345 | tail -1 # 更细的内存映射汇总
# === ★ 第四步:Java 等带虚拟机的,还要看它自己的内存 ===
# ★ Java 进程的 RSS,= JVM 堆 + 元空间 + 线程栈 +
# 堆外内存 + JVM 自身。光看 RSS 大,不知道大在哪。
$ jcmd 12345 GC.heap_info # 看 JVM 堆
$ jcmd 12345 VM.native_memory summary # 看 native 内存(需开 NMT)
# ★ Java RSS 涨,可能是堆,也可能是堆外 / 元空间 /
# 线程过多 —— 要进一步分。
# === 认知 ===
# ★ available 真的低了才查进程:ps aux --sort=-rss
# 揪大户。★ 区分 VSZ(申请的虚拟空间,虚高,忽略)
# 和 RSS(真占的物理内存,只看它)。"泄漏"的定义
# 是某进程 RSS 随时间持续只涨不降 —— 必须连续观察
# 一段时间看趋势,单看一时刻 RSS 大不算泄漏。Java
# 等还要用 jcmd 进一步分堆 / 堆外 / 元空间。
修复 5:内存真不够时,系统会有什么表现
# === ★ 内存真到危险线,系统会怎样,怎么识别 ===
# === ★ 信号 1:available 持续走低,逼近 0 ===
# ★ 这是最早的预警。光 used 大不算,available 一路
# 下滑、收不住,才是真的在恶化。
# === ★ 信号 2:开始大量用 swap ===
# ★ free 不够,内核会把一部分【不常用】的内存页,
# 写到磁盘上的 swap 分区,腾出物理内存。
$ free -h # 看 Swap 行的 used
$ vmstat 1 # ★ 看 si / so 两列
# si = swap in(从 swap 读回),so = swap out(换出)
# ★ si / so 持续 > 0,而且数值不小 -> 系统在频繁
# swap,内存吃紧了。★ swap 走磁盘,极慢,一旦
# 频繁 swap,整机性能会断崖式下跌。
# === ★ 信号 3:★ OOM Killer 出手杀进程 ===
# ★ 内存真的山穷水尽(swap 也满了),内核会启动
# OOM Killer(Out Of Memory 杀手):它【挑一个
# 进程,直接杀掉】,强行腾内存,保住整个系统。
# ★ 被杀的进程,会"莫名其妙"地消失。怎么确认是
# OOM Killer 干的:
$ dmesg | grep -i 'killed process'
# Out of memory: Killed process 12345 (java) ...
$ dmesg | grep -i 'oom'
# ★ 看到这种,就是内存真的爆了,OOM Killer 杀了人。
# === ★ OOM Killer 挑谁杀 ===
# ★ 它按一个 oom_score 打分,大致是"占内存越多、
# 越该死"。所以最吃内存的那个(往往就是你的主
# 服务),最容易被它选中 —— 这很讽刺但合理。
$ cat /proc/12345/oom_score # 看某进程的 oom 评分
# === ★ 对比:本文事故,以上信号一个都没有 ===
# ★ available 13G,健康;swap used 0,没碰 swap;
# dmesg 里没有任何 oom 记录。
# ★ ★ 三个真·危险信号全是阴性 —— 这本身就足以证明:
# 内存【根本没问题】。我那一夜的恐慌,毫无根据。
# ★ 教训:判断内存有没有事,看的是 available、swap
# 活动、oom 日志这三样,【不是】free 里的 used。
# === 认知 ===
# ★ 内存真不够的三个信号:① available 持续走低逼近
# 0;② 频繁 swap(vmstat 的 si/so 持续 >0,整机
# 会断崖变慢);③ ★ OOM Killer 杀进程(dmesg 里
# "Killed process",它挑占内存最多的杀)。判断内存
# 有没有事,认这三样 —— 本文这三项全阴性,证明内存
# 健康,used 大纯粹是缓存虚高。
修复 6:Linux 内存排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 判断内存够不够,只看 free 的 available 列,不看 used / free ===
$ free -h
# === 2. ★ used 大不等于内存紧张,它常把可回收缓存算进去虚高 ===
# === 3. ★ free 列小是健康机器的正常表现,内核会用掉几乎所有空闲内存做缓存 ===
# === 4. ★ buff/cache 是页缓存,可回收,进程要内存内核会立刻丢缓存让座 ===
# === 5. 验证缓存可回收:sync; echo 3 > drop_caches(★ 仅排查用,别在生产常做)===
# === 6. ★ 排查进程内存看 RSS 不看 VSZ,VSZ 是虚拟空间虚高 ===
$ ps aux --sort=-rss | head
# === 7. ★ 泄漏是趋势:RSS 随时间持续只涨不降,单看一时刻大不算泄漏 ===
# === 8. 内存真危险的信号:available 逼近 0、频繁 swap、dmesg 有 oom ===
$ vmstat 1 ; dmesg | grep -i oom
# === 9. OOM Killer 会挑占内存最多的进程杀,被杀进程会莫名消失,查 dmesg 确认 ===
# === 10. 排查"内存是不是有问题"的步骤链 ===
$ free -h # ① 看 available,不是 used
$ vmstat 1 # ② 看 si/so 有没有频繁 swap
$ dmesg | grep -i 'killed process' # ③ 有没有 OOM 杀进程
$ ps aux --sort=-rss | head # ④ available 真低了才查进程 RSS
# available 充足 + 无 swap + 无 oom = 内存健康,used 大是缓存,别瞎查。
命令速查
需求 命令
=============================================================
看内存概况 free -h
★ 看真正可用内存 free -h 的 available 列
看可用内存(底层) grep MemAvailable /proc/meminfo
看缓存构成 grep -E 'Cached|Buffers' /proc/meminfo
按物理内存排序进程 ps aux --sort=-rss | head
看某进程真实内存 cat /proc/PID/status | grep VmRSS
看 swap 活动 vmstat 1(看 si / so 列)
看是否发生过 OOM dmesg | grep -i 'killed process'
看某进程 OOM 评分 cat /proc/PID/oom_score
手动丢弃缓存(★ 仅排查) sync && echo 3 > /proc/sys/vm/drop_caches
口诀:free 的 used 大不要慌 那多半是内核拿空闲内存做的可回收缓存
判断内存够不够只看 available,真危险才有 swap 飙升和 OOM 杀进程
避坑清单
- 判断内存够不够只看 free 的 available 列,不要看 used 也不要用 total 减 used 算剩余
- free 里的 used 历史上常把可回收缓存算进去会虚高吓人,used 大不等于内存紧张
- free 列小是健康机器的正常表现,内核会主动用掉几乎所有空闲内存做页缓存加速 IO
- buff/cache 主要是页缓存,它占着内存但随时可回收,进程一要内存内核立刻丢缓存让座
- 缓存内存不会饿死任何进程,空闲内存不拿来做缓存才是浪费,这是内核加速不是故障
- 排查进程内存看 RSS 常驻内存,不看 VSZ 虚拟空间,VSZ 包含没真正用到的部分会虚高
- 内存泄漏是趋势:某进程 RSS 随时间持续单调只涨不降,单看一个时刻 RSS 大不能叫泄漏
- 内存真危险的信号是 available 持续逼近 0、vmstat 的 si so 频繁大于 0、dmesg 有 oom 记录
- OOM Killer 内存山穷水尽时会挑占内存最多的进程直接杀掉,进程莫名消失查 dmesg 确认
- drop_caches 手动丢缓存只用于验证排查,生产环境别常做,丢了缓存磁盘 IO 会暂时变慢
总结
这次"熬一整夜抓一个不存在的内存泄漏"的事故,纠正了我一个关于"占用"的、藏得极深的错觉。在我过去的脑子里,内存的状态,只有【非黑即白】的两种:一种叫"空闲",意思是"没人要、随便用";另一种叫"已用",意思是"被占走了、回不来了"。在我这套二分法里,free -h 那个 used: 15G,意思板上钉钉——有 15G 内存,落进了"已用"这个黑洞,被某个东西【拿走且不归还】了。剩下的事顺理成章:既然 15G 被"拿走不还",那一定有个进程在偷偷吞噬,这就是"泄漏"。我那一整夜的导堆、看 GC、查引用,全部建立在这个二分法之上。可天亮时我对上的那笔账,把这套二分法砸得粉碎:所有进程的内存加起来才 4G,而 used 说 15G——中间整整 11G,既不"空闲"(free 把它算进了 used),又不被任何进程"已用"(没有一个进程认领它)。它卡在我那套黑白二分法【根本没有的第三个格子】里。我被逼着去面对它,才终于看清:内存的状态,从来不是两种,是【三种】。除了"空着的"和"被进程占死的",还有第三种,也是健康 Linux 上占比最大的一种——"被内核拿去做缓存、但你一要就立刻还你的"。这第三种内存,它【占用着】,却没有【被用掉】。"占用"和"用掉",这两个我一直当成同义词的词,在这里裂开成了天差地别的两件事。页缓存就站在这道裂缝上:它实实在在地占着那 11G,所以 free 不肯把它算作空闲;但它又随时准备让座,所以它对任何一个真正需要内存的进程来说,等于不存在。它是一个"占着座位、却随叫随到地起身"的人——你说他占了座吗?占了。你说这座位你坐不了吗?你随时能坐。复盘到根上我才明白,我那一夜的恐慌,根源不是技术不熟,而是我脑子里那把尺子,刻度太少。我只有"有"和"无"两格,于是我被迫把一切"看起来被占着"的东西,都塞进"无"那一格,然后为这个虚构的"无"惊慌失措。这次最大的收获,是我学会了对一切"占用类"的数字,多问一个问题:它是被【占死了】,还是只是被【暂用着】?这个区别,几乎无处不在。一个连接池显示"占用 80 个连接",这 80 个是真有 80 个请求在跑,还是大多是空闲待命、随时能复用的?一块磁盘"用了 90%",这 90% 是不可删的核心数据,还是一半都是能随时清的缓存和日志?一个线程池"满了",是真有那么多活在干,还是线程只是占着没退?这些数字,几乎全都和内存一样,藏着那个"被占用但可回收"的第三态。而绝大多数监控、绝大多数人的直觉,都只给你"用了多少"这一个数——它最大、最显眼、也最会骗人。真正该追问的,永远是另一个数:在不伤害任何人的前提下,你现在【还能拿出来多少】?对内存,这个数叫 available。我熬那一夜,不是因为我看错了一个数字,是因为我从没想过,我该看的【根本是另一个数字】。从此我对任何一个"占用率"都会留个心眼:先别急着为那个高高的"已用"心跳加速,先找一找,有没有一个安安静静的、叫"可用"的数——那个数,才是系统真正想对我说的话。
—— 别看了 · 2026