一、SWAP 不是“垃圾站”,而是“冷藏库”
很多运维把 SWAP 当成“内存垃圾站”,认为它只是物理内存不足时的临时倾倒场。实际上,SWAP 更接近“冷藏库”:内核会把“最近很少访问”的匿名页(堆、栈、共享内存)压缩后搬到磁盘,让出物理内存给更急需的进程。搬出过程称为换出(swap-out),再次访问时触发缺页异常,把页从磁盘读回内存,称为换入(swap-in)。换出/换入本身不致命,但伴随随机磁盘 I/O,延迟从纳秒级骤升到毫秒级,导致“物理内存有空闲,系统却很卡”的诡异现象。理解“冷藏库”本质,就能明白:SWAP 使用率高的根源,不一定是“内存不够用”,也可能是“内核觉得这些页不值得留在内存”。
二、内存水位与回收阈值:什么时候才往“冷藏库”搬货
内核通过水位线(watermark)把物理内存划分为三个区域:HIGH、LOW、MIN。当剩余内存跌到 LOW 水位,后台回收线程 kswapd 被唤醒,尝试释放页缓存、匿名页、 slab 缓存;若继续跌到 MIN 水位,会触发“直接回收”,申请内存的进程自己进入回收逻辑,此时再分配不到页,就会选择“换出”匿名页。换句话说:SWAP 的写入,不一定等到“内存耗尽”,只要“水位低于预期”就可能发生。于是出现“8 GB 空闲,却疯狂换出”的怪象——也许是水位线被手动调低,也许是某进程瞬间申请大块内存,把水位瞬间砸穿。
三、匿名页、页缓存、Slab:谁最容易被“冷藏”
匿名页(AnonPages)没有文件后端,换出只能进 SWAP;页缓存(PageCache)有文件后端,回收时直接丢弃,下次读文件再加载,无需 SWAP;Slab 是内核数据结构缓存,回收代价小。因此,SWAP 里“住”的绝大部分是匿名页。若应用大量使用 malloc/new、创建大数组、使用匿名共享内存,就等于给“冷藏库”持续供货。反观 PageCache,除非手动 drop,否则内核更倾向于丢弃它而非搬进 SWAP。定位 SWAP 暴涨时,第一步就是区分“匿名页激增”还是“PageCache 被挤占”,前者聚焦应用内存泄漏,后者关注文件读写模式。
四、工具链:从 top、vmstat 到 /proc/meminfo 的“破案路线”
top 的 RES 代表进程常驻内存,SWAP 列显示该进程被换出的量;vmstat 的 si/so 每秒换入/换出页数,是“实时冷藏速度”晴雨表;/proc/meminfo 的 AnonPages、SwapCached、SlabReclaimable 给出全局分布。若 si/so 持续大于 200 页/秒,且 AnonPages 不断缩小,SwapCached 增加,即可判定“正在积极换出”。进一步用 `perf top -p $PID` 观察缺页异常占比,若 `page_fault` 高居榜首,说明进程频繁访问已被换出的内存,触发“换入”风暴,导致 CPU 空转在磁盘 I/O 上。
五、缺页异常与“换入”风暴:为什么物理内存有空闲,系统却很卡
进程访问被换出的页,会触发 major page fault,内核同步读磁盘,把页重新搬进内存。若应用访问模式随机(如大数组遍历),且 SWAP 分布在机械盘不同扇区,就会产生大量随机读,磁盘 seek 时间叠加,导致“CPU 利用率低、load 高、响应慢”的怪相。SSD 能缓解 seek,却无法避免内核锁竞争:换入过程需要加锁修改页表,多线程并发缺页时,锁争用会把延迟再次放大。因此,SWAP 使用率只是“表面温度”,缺页异常率才是“真正热度”。
六、应用层泄漏:内存泄漏的“SWAP 化”表现
Java 应用出现内存泄漏,Old 区持续增长,GC 无法释放,最终触发 `OutOfMemoryError`;但在到达 OOM 之前,内核会把 Old 区的匿名页陆续换出,于是 SWAP 使用率一路飙升,GC 停顿时间也变长——因为每次 Major GC 都要把换出的页重新读回,再扫描标记。C/C++ 应用若忘记 free,匿名页同样永不回收;Python、Node.js 的对象循环引用,也会让堆区膨胀。定位思路:
- Java:jstat 观察 Old 区增长曲线,jmap dump 后 MAT 分析 GC Root;
- C/C++:Valgrind 或 AddressSanitizer 跑压测,找出未释放块;
- Python:tracemalloc 统计内存增量,检查 __del__ 循环引用。
解决泄漏后,SWAP 使用率往往“断崖式”下降,验证“匿名页激增”才是元凶。
七、内核调优:水位线、swappiness、oom_score 的“三角平衡”
swappiness=60 是默认妥协值:0 表示“除非内存耗尽,否则不 SWAP”;100 表示“积极 SWAP”。调低 swappiness 能减少换出,但可能导致“内存耗尽时直接 OOM”;调高则让 SWAP 提前参与,换取更大文件缓存。经验法则:
- 数据库、缓存、消息队列:swappiness=10,尽量保留内存给缓存;
- 计算密集型、短期任务:swappiness=60,默认即可;
- 嵌入式、小内存:swappiness=100,充分利用 SWAP,避免 OOM 杀死关键任务。
再配合 `/proc/$PID/oom_score_adj`,让不重要进程更容易被 OOM Kill,减少“同归于尽”概率。三角平衡的核心是:让 SWAP 成为“有选择”的冷藏,而非“随机”的倾倒。
八、应用改造:减少匿名页的四把“手术刀”
1. 对象池:避免频繁 malloc/new,把短生命周期对象变成长生命周期池,减少匿名页波动;
2. 堆外内存:Java 的 DirectByteBuffer、C 的 mmap,把内存映射到文件,而非匿名页,换出时直接丢弃,无需 SWAP;
3. 压缩算法:启用 zswap,在内存里先压缩匿名页,再决定是否搬去磁盘,可减少 30~50% 换出量;
4. 本地缓存转远程:把本地大缓存搬到 Redis 等外部节点,让“高频但可重建”的数据离开匿名页,既减少 SWAP,也降低单点内存压力。
四把手术刀并非“一刀切”,而是根据业务特性“能池化就池化,能映射就映射,能压缩就压缩,能外迁就外迁”。
九、监控与告警:把“冷藏库”变成“透明冷库”
SWAP 使用率≠绝对指标,需要组合监控:
- 匿名页占比:超过 60% 且持续增长,预示泄漏;
- 缺页异常速率:major fault > 100/s 持续 5min,预示“换入”风暴;
- load 与 CPU 利用率背离:CPU 低、load 高,典型换入阻塞;
- 进程 swap 量:/proc/$PID/status 的 VmSwap 字段,单进程超过 500MB 即告警。
把以上指标接入时序数据库,配合“预测型”告警(如“匿名页七天同比增长 30%”),能在“真正卡死”前提前介入,把“事后救火”变成“事前干预”。
十、真实案例:一次“8 GB 空闲,SWAP 7 GB”的追凶笔记
生产环境 32 GB 物理内存,剩余 8 GB,SWAP 却占用 7 GB,应用响应极慢。top 显示某 Java 进程 RES 仅 6 GB,但 /proc/$PID/status 里 VmSwap 高达 5 GB。jstat 发现 Old 区占用 90%,Major GC 每秒一次。进一步用 jmap 看堆 histogram,发现某缓存类实例 600 万条,占 12 GB。结论:缓存泄漏 → Old 区暴涨 → 内核换出 → Major GC 触发缺页异常 → 随机读磁盘 → load 飙高。解决:缩小缓存 TTL,把 12 GB 降到 3 GB,SWAP 瞬间释放 5 GB,响应时间恢复。案例印证:SWAP 高只是“症状”,匿名页泄漏才是“病根”,调优 swappiness 治标不治本,减少匿名页才是核心。
十一、常见误区:这些“急救动作”可能越救越忙
- 盲目 drop_caches:echo 3 > drop_caches 只能释放 PageCache,对匿名页无效,反而让文件读请求直接落盘,增加 IO 压力;
- 直接关闭 SWAP:swapoff -a 会把换出的页全部读回内存,若匿名页高达数十 GB,会导致几分钟“假死”,甚至触发 OOM;
- 一味调低 swappiness:从 60 调到 10,看似减少换出,但内存耗尽时 OOM 杀手随机扫射,可能把数据库进程杀死,代价更大;
- 加大物理内存却不改应用:内存泄漏速度随内存增大而加快,最终 SWAP 依旧爆满,只是时间延后。
正确顺序:先定位匿名页来源 → 解决泄漏/压缩/外迁 → 再调 swappiness → 最后才考虑加内存或关 SWAP。
十二、未来趋势:从“磁盘 SWAP”到“内存压缩”与“分布式换页”
Linux 5.x 引入 zswap、zram,把匿名页压缩后留在内存,而非搬到磁盘,I/O 延迟从毫秒级降到微秒级;云原生场景下,出现“分布式换页”——把冷页压缩后放到远端 NVMeoF 存储,节点本地几乎无 SWAP,却拥有百倍于磁盘的换页带宽。技术演进并未改变“换出”本质,只是让“冷藏库”离 CPU 更近。掌握传统 SWAP 调优思路,才能在新技术来临时,迅速理解“压缩率 vs. CPU”、“远端带宽 vs. 本地内存”的权衡逻辑。
SWAP 使用率过高,从来不是“磁盘不够大”,而是“内存用法不对”。理解水位线、匿名页、缺页异常、回收策略,才能把“冷藏库”变成“可控冷库”:该冷藏的冷藏,该丢弃的丢弃,该外迁的外迁。下一次监控大屏跳出 SWAP 90% 时,你不再匆忙重启,而是气定神闲地打开工具链,沿着“水位→匿名页→缺页→泄漏”的路线,一步步找到那个贪吃的进程,然后优雅地掐掉它的“内存水龙头”。愿你的系统,再也不会被“隐形内存”拖垮,而是让每一份物理内存都用在真正有价值的计算上。