图 1:本文封面,三层排查动线从系统层一路钻到应用层。
同批的面试题解析把 free 输出里 buffers 与 cached 两个列名的语义掰开讲透了——那篇解决「看不懂」,这篇解决「然后呢」:一台机器的内存水位告警响了,从看到数字到定位到代码,中间隔着一条三层排查链。系统层看够不够,进程层看是谁,应用层看为什么。把每一层的核心指标和工具串起来,「内存去哪儿了」就不是玄学。
第一层 系统层:先看总量与趋势,回答「够不够」。
系统层有三件工具,各自回答不同的问题。free -m 是快照:重点看 available 而不是 free——available 是内核估算的「不触发 swap 还能给新应用分配多少」,约等于空闲内存加可回收的页缓存;free 小、cache 大是 Linux 的常态,不是泄漏。vmstat 1 是趋势:盯 swap 两列,si(每秒换入)与 so(每秒换出)持续非零,说明内存真的不够周转、开始依赖磁盘换页,这比任何静态数字都更能宣告「紧张」。/proc/meminfo 是原始底账:free 和 vmstat 的数字全部来自这里,MemAvailable、Buffers、Cached、SwapCached、SReclaimable 等字段可以按自己的口径自由组合,写监控脚本时直接解析它比解析 free 的输出稳定得多。
系统层的产出是一个判断:available 充裕、si/so 为零,内存就不是瓶颈,告警大概率是阈值配错;available 持续走低或在换页,才值得往下钻。
第二层 进程层:找到大户,回答「是谁」。
确认内存紧张后,下一步是把账算到进程头上。最常用的入口是 ps aux --sort=-rss,按 RSS(常驻内存集)排序,排在最前面的就是嫌疑大户。但 RSS 有一个系统性偏差:共享库、共享内存会被每个映射它的进程各算一遍,几个进程加起来可能远超物理内存。要精确归因,去读 /proc//smaps_rollup 里的 Pss(比例内存集)——内核把每块共享映射按映射它的进程数分摊,所有映射的 Pss 加起来就是这个进程对内存的「真实负债」,全系统 Pss 求和基本等于实际占用。粗排用 RSS,定责用 Pss,两者配合不会被共享库误导。
第三层 应用层:拆开大户,回答「为什么」。
找到大户只是开始,同一份 RSS 背后可能是完全不同的故事。以 Java 服务为例,一个 6 GB 的 RSS 通常由几块构成:堆内存(看 -Xmx 与 GC 日志)、堆外内存(直接缓冲区、堆外缓存,jcmd 的 Native Memory Tracking 可查)、线程栈与连接池(线程数乘以栈大小)、元空间与 JIT 代码缓存。应用层的工具因技术栈而异,但思路一致:把进程的 RSS 拆解成几个有名有姓的池子,哪个池子在涨就查哪个。涨在堆上可能是缓存无界或对象泄漏,涨在堆外可能是直接缓冲区没释放,涨在线程栈上往往是连接池配置失控——定位到池子,问题就已经解决了一半。
用一段代码把三层动线跑一遍。
本机没有 Linux 环境,下面用 Node.js 22.15.0 内置一份 meminfo 样本和进程表,把三层排查各自动作模拟出来(node mem-chain.js):
const meminfo = `MemTotal: 16384000 kB
MemFree: 512000 kB
MemAvailable: 4200000 kB
Buffers: 128000 kB
Cached: 3400000 kB
SReclaimable: 320000 kB`;
// 第一层 系统层:够不够
const m = {};
for (const line of meminfo.trim().split('\n')) {
const [k, v] = line.split(/:\s+/);
m[k] = parseInt(v, 10);
}
console.log(`[系统层] free 仅 ${(m.MemFree / 1024).toFixed(0)} MB,但 available 有 ${(m.MemAvailable / 1024).toFixed(0)} MB`);
// 第二层 进程层:是谁
const processes = [
{ name: 'java-app', rss: 6200 }, { name: 'postgres', rss: 1400 },
{ name: 'nginx', rss: 120 }, { name: 'node-svc', rss: 890 }, { name: 'sshd', rss: 8 },
];
processes.sort((a, b) => b.rss - a.rss);
console.log('[进程层] 按 RSS 排序:' + processes.map(p => `${p.name}=${p.rss}MB`).join(', '));
// 第三层 应用层:为什么
const app = { 'JVM 堆': 4096, '堆外缓存': 1200, '线程栈+连接池': 500, '元空间等': 404 };
const sum = Object.values(app).reduce((a, b) => a + b, 0);
for (const [k, v] of Object.entries(app)) console.log(` ${k}: ${v} MB`);
console.log(`[应用层] 合计 ${sum} MB ≈ ${processes[0].name} 的 RSS`);
实测输出:
[系统层] free 仅 500 MB,但 available 有 4102 MB
[进程层] 按 RSS 排序:java-app=6200MB, postgres=1400MB, node-svc=890MB, nginx=120MB, sshd=8MB
JVM 堆: 4096 MB
堆外缓存: 1200 MB
线程栈+连接池: 500 MB
元空间等: 404 MB
[应用层] 合计 6200 MB ≈ java-app 的 RSS
输出演示了完整动线:系统层先排除假警报(free 只剩 500 MB 看着吓人,available 其实还有约 4 GB);假设告警属实,进程层一步定位到 java-app 这个 6.2 GB 的大户;应用层把它拆成四个池子,每一块都能对上号——三层各答一问,链条闭环。
图 2:四张工具卡与四步排查动线的对应关系。
把动线固化成肌肉记忆。
这套链条的价值在于顺序不可颠倒:不先在系统层确认「真紧张」,就直接去揪进程,常常揪出来的是无辜的页缓存;不先在进程层锁定大户,就直接上应用层剖析工具,等于大海捞针。训练与推理机器尤其要把告警阈值设在 available 加水位余量上——只盯 free 列的告警策略,会在每次数据集预热把页缓存顶起来时误报一轮。
最后留一个练习:下次遇到内存告警,先别重启服务,按「available → vmstat 的 si/so → RSS 排序 → Pss 定责 → 应用层拆池」走一遍,看看链条在哪一环给出答案?你实测过的排查路径和这个顺序一致吗,欢迎分享你的版本。