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-ng(free命令所属的软件包,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里就有多大的水分。
第二处是正在回写的脏页。Dirty和Writeback字段不为零的时候,对应的页面不能立刻丢弃,得先落盘。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 | 完全空闲的物理页 | 能,但它只是可用量的下限 |
| shared | tmpfs / 共享内存占用(新版已移到 buff/cache 里) | 基本不能,除非有 swap |
| buff/cache | 块设备缓冲 + 页缓存 | 大部分能,但要扣掉 Shmem 和不可回收 slab |
| available | 内核估算的可用量 | 以它为准 |
几个实用参数值得记住:
free -m/free -g:按 MB / GB 显示,看大数更直观。free -s 2 -c 5:每 2 秒刷新一次,共 5 次。判断内存是在涨还是在跌,单次采样没意义,必须看趋势。free -w:把buffers和cache拆成两列单独显示(需要 procps-ng 3.3.12 以上),排查 IO 相关问题时有用。free -t:最后多一行 total,把内存和 swap 加起来。
注意:
available列在老旧系统上可能不存在。CentOS 6 那一代机器(内核 2.6.32)的free就没有这一列,得自己估算,方法见 3.1 节。
2.2 /proc/meminfo:所有内存数字的源头
free、top、vmstat这些命令显示的内存数据,追根溯源全部来自/proc/meminfo。想搞清楚任何一个数字是怎么来的,直接cat /proc/meminfo | head -n 20,单位是 kB。
常用字段我挑出来列一下,都值得记:
| 字段 | 说明 |
|---|---|
| MemTotal | 可用物理内存总量,比标称小是因为内核镜像、硬件保留区、部分驱动先占了一部分 |
| MemFree | 完全空闲的页 |
| MemAvailable | 内核估算的可用量 |
| Buffers | 块设备元数据缓存,通常不大 |
| Cached | 文件页缓存,包含 Shmem |
| Shmem | tmpfs 和共享内存,Cached里的水分所在 |
| SReclaimable / SUnreclaim | 可回收 / 不可回收的内核 slab |
| Active(anon/file)、Inactive(anon/file) | 四个 LRU 链表的页数,available的计算基础 |
| Dirty / Writeback | 待写回 / 正在写回的脏页 |
| SwapTotal / SwapFree | 交换区总量和剩余 |
| AnonPages | 匿名页,进程堆栈占的,基本不可回收 |
| Mapped | 被 mmap 映射的文件页 |
| PageTables | 页表本身占的内存,大内存机器上能到几个 GB |
| CommitLimit / Committed_AS | overcommit 机制下的承诺上限和已承诺量 |
提取单个字段用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_rollupRssAnon(匿名页)和RssFile(文件映射页)分开看,是判断"这个进程是真吃内存还是只是映射了一堆文件"的关键,后面 4.2 节会展开。
2.4 vmstat 和 slabtop:看趋势和内核态占用
单点采样只能说明"现在",判断内存压力要看趋势。vmstat是干这个的:
vmstat 1 10重点看两列:si(从 swap 换入)和so(换出到 swap)。这两列持续非零,说明内存真的紧张了,内核已经在把匿名页往 swap 上倒腾。偶尔蹦出来一次不为零无所谓,那是内核在做平衡;连续十几个采样周期都是几百上千,就说明物理内存不足以支撑当前工作集。
free列在vmstat里也很直观,但注意vmstat的buff和cache是分开的两列,加起来对应free输出里的buff/cache。
另一个常被忽略的方向是内核自己吃掉的内存:
slabtop -o -s c这条命令按对象大小排序显示内核 slab 缓存。dentry和inode这两项通常最大,它们属于SReclaimable,内存紧张时能自动回收,不用管。但如果SUnreclaim那一列很大,说明有内核模块在漏内存,这种只能靠重启或者卸载模块解决,应用层怎么优化都没用。我就遇到过一次某监控 agent 的内核模块持续申请 socket 相关 slab 不释放,available一天掉一点,最后是靠slabtop对比两天快照定位到的。
3. 几个容易算错的指标和算法
3.1 老系统没有 MemAvailable,怎么自己估
内核 3.14 之前的系统没有MemAvailable,free命令即使升级了也只能显示+/- 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 pagecache、free、slab各多少),还会列出所有候选进程的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 每天涨一点,三个月后要重启一次,到底是代码泄漏还是正常的缓存行为。
判断方法其实很明确:看RssAnon和RssFile的比例变化。
# 采样两次,间隔几小时,对比匿名页的增长 awk '/RssAnon|RssFile|RssShmem|VmRSS/{print}' /proc/<pid>/statusRssFile增长基本可以放心,那是文件页缓存被算进了进程的 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 日志就知道是不是满;真正容易失控的是堆外——DirectByteBuffer、Metaspace、JIT 编译缓存、线程栈。堆外内存泄漏的特征是:堆用得不多,但进程 RSS 一直涨,-XX:MaxDirectMemorySize没设置的话默认等于堆大小,很容易超。排查用Native Memory Tracking:
java -XX:NativeMemoryTracking=summary -jar app.jar jcmd <pid> VM.native_memory summaryRedis 也有类似的坑。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_bytes和memory.limit_in_bytes。
memory.stat里的anon和file分得很清楚,比/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 oom | OOM Killer | 分析内存增长来源 |
| RSS 只涨不跌 | RssAnon变化趋势 | 堆内存泄漏 | pmap/ NMT 定位代码 |
| available 够但 malloc 失败 | ulimit -v、cgroup 限制 | 进程级或 cgroup 配额 | 调整限制或 overcommit |
| buff/cache 高但回收不动 | Shmem、SUnreclaim | tmpfs 占用、内核泄漏 | 清理 tmpfs 或重启 |
| 容器内 free 数字离谱 | memory.current | procfs 未隔离 | 读 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 -h看available,再vmstat 1 5看si/so,然后ps --sort=-rss看谁在吃。三步走完,二十秒内基本能得出结论,剩下的就是照着上面几个坑逐条排除了。