《Linux 内存管理:分页、交换、回收与 OOM》

系统讲解 Linux 内存管理:虚拟内存与分页机制、页表与 HugePages、slab 与伙伴系统、swap 交换与 LRU 回收、OOM killer 与 cgroup 内存限制、NUMA 内存策略、vm.swappiness 等调优参数,以及 free/ps/vmstat/smem 故障排查与内存泄漏实战。

引言

进程看到的是连续、独立、巨大的虚拟地址空间,底层却共享有限的物理内存——Linux 靠分页、页表、回收与交换在这两层之间搭桥。内存不足时内核先回收缓存、再换出匿名页(swap),实在撑不住就交给 OOM killer 杀进程止损。本文按「机制 → 参数 → 排查」的路径讲透内存管理:从虚拟内存与分页、页表与 HugePages、slab 内核缓存,到 swap 与 OOM、NUMA 与 cgroup 限制,最后用 free/ps/vmstat/smem 实战定位内存泄漏。

前置:/linux-process-management/(进程与 /proc 监控基础)。内核参数调优参考 /linux-kernel-tuning/。


目录


1. 虚拟内存与分页机制

每个进程都有独立的虚拟地址空间,由内核的页表把它映射到物理内存——这就是「多进程隔离 + 共享物理内存」能同时成立的原因。

# 页面大小(几乎总是 4096 字节)
getconf PAGE_SIZE
# 当前进程的虚拟内存布局(地址区间 → 权限 → 文件映射)
cat /proc/self/maps | head -10
# 系统内存总量
grep -E 'MemTotal|MemAvailable' /proc/meminfo

页表把虚拟页(4KB)映射到物理页帧,缺页时才真正分配物理页——这叫按需分页(demand paging)。程序刚启动时绝大部分页并未分配,物理内存只在真正访问时才被占上。

概念含义
虚拟地址空间进程眼里连续的大空间(x86-64 用户态 128 TB)
物理页帧实际占用 RAM 的最小单位(4KB)
页表虚拟页 → 物理页帧的映射表(每进程一张)
缺页异常访问未映射页,内核现场建映射/读盘

心智:ps 里 VSZ 是虚拟内存、RSS 才是真实占用。进程「吃了几十 G」多半是虚拟空间大、物理页没真分配——别被吓到,先看 RSS。

# 统计进程的缺页次数(minor 缺页=直接在内存解决,major 缺页=要从磁盘读)
ps -o pid,minflt,majflt,cmd -p 1234

记忆:minor fault 常见且无害(页已在页缓存/内存),major fault 要读磁盘才致命。major fault 飙升说明工作集超出物理内存、正在频繁换入换出。


2. 页表与 HugePages

4KB 小页在内存巨大时页表膨胀、TLB 命中率下降——大页(HugePages)用更大粒度换更少 TLB miss。

# 查看大页配置与用量
grep -E 'HugePages_Total|Hugepagesize' /proc/meminfo
# 预留 2MB 大页
sysctl -w vm.nr_hugepages=1024
# 挂载 hugetlbfs 供应用使用
mkdir -p /mnt/huge && mount -t hugetlbfs hugetlbfs /mnt/huge

透明大页(THP)让应用无需感知就能用上 2MB 大页,但后台 khugepaged 合并时会带来延迟毛刺:

# THP 三种模式:always / madvise / never
cat /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled   # 延迟敏感场景常关
页面大小典型用途效果
4KB默认小页灵活、省内存、TLB miss 多
2MBHugeTLB/THPTLB 覆盖范围大 512 倍,适合大内存应用
1GBHugeTLB数据库/虚拟化大内存场景,启动前预留

记忆:数据库、JVM、DPDK 这类「内存大 + 随机访问密集」的程序收益最明显。开启前先 grep Huge /proc/meminfo,预留的 HugePages 不再参与普通回收,配置不当反而浪费内存。


3. slab 与内存碎片

内核自己的小对象(dentry、inode、task_struct)由 slab/slub 分配器管理,粒度远小于 4KB 页——这也让「内核内存」的统计方式与用户态完全不同。

# slab 缓存用量(每行一种内核对象)
cat /proc/slabinfo | head -15
# 伙伴系统的剩余块(order-0 到 order-10,小块不够时就是碎片)
cat /proc/buddyinfo

slab 缓存按对象类型预建:dentry 对应文件路径缓存、inode 对应文件元数据、kmalloc-* 是通用小块。文件数量极多时 dentry/inode 缓存会显著吃内存。

# 手动触发内存压缩(把碎片整理成连续大块,生产慎用)
echo 1 > /proc/sys/vm/compact_memory
# 控制内核回收 dentry/inode 缓存的倾向(默认 100)
sysctl -w vm.vfs_cache_pressure=200

心法:碎片化 ≠ 泄漏。free 里 available 还很高、但分配大块连续内存失败(比如 THP 开启失败、驱动分配失败),才要考虑碎片。vfs_cache_pressure 调高可让内核更积极回收文件缓存。


4. swap 交换与内存回收

物理内存不够时,内核把不常用的匿名页换到磁盘(swap),需要时再换回——回收的顺序是「先缓存、再匿名页、最后 OOM」。

# 创建并使用 swapfile(或分区)
fallocate -l 4G /swapfile
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
# 查看 swap 用量
swapon --show
free -h

内核回收由 kswapd 在后台按水位线驱动;vm.swappiness(0-100,默认 60)决定「多积极地把匿名页换出」而非回收页缓存:

swappiness表现
0几乎不主动换出匿名页,宁回收页缓存(桌面/响应优先)
10~30仅内存压力明显时才换出(推荐服务器)
60默认:匿名页与页缓存接近一视同仁
100非常积极换出匿名页
sysctl -w vm.swappiness=10          # 临时
echo 'vm.swappiness=10' >> /etc/sysctl.conf   # 持久

记忆:swap 不是「内存不够的罪证」,而是最后的弹性缓冲。完全不设 swap 的服务器一旦内存打满,直接进入 OOM,连缓解的机会都没有。zswap 用压缩内存做 swap 的「第一层」,可减少真实磁盘换入换出。


5. OOM killer 与 cgroup 内存限制

当回收也救不了内存时,内核根据 oom_score 选择「牺牲」一个进程——得分越高越容易被杀,可由 oom_score_adj 手动调整。

# 查看进程的 OOM 得分与可调值
cat /proc/1234/oom_score /proc/1234/oom_score_adj
# 保护关键进程(-1000 = 永不 OOM 杀掉)
echo -1000 > /proc/1234/oom_score_adj
# OOM 发生时内核日志
dmesg -T | grep -i oom

cgroup 把内存限制从「整机」细化到「一组进程」,是容器隔离的基础(配合 /linux-containers-isolation/):

# cgroup v2:限制该组最多 512MB
echo 512M > /sys/fs/cgroup/app/memory.max
echo 128M > /sys/fs/cgroup/app/memory.high   # 超过则积极回收
cat /sys/fs/cgroup/app/memory.events          # 看是否触发过回收/OOM
层面超限后果
整机无 swap内核全局 OOM,随机杀分
cgroup v1 memory.limit_in_bytes组内 OOM,杀组内进程
cgroup v2 memory.max组内 OOM,可配 memory.oom.group 整组杀
systemd-oomd依据 memory.pressure 提前杀,避免真 OOM

心法:生产环境「内存打满」的合理顺序是:回收页缓存 → 换出 swap → 杀超限容器(cgroup OOM)→ 全局 OOM。看 dmesg 里是 Out of memory: Killed process 还是 Memory cgroup out of memory,就知道是整机还是容器的问题。


6. NUMA 架构与内存策略

多路服务器上,CPU 访问「本地内存」比访问「远端内存」快——NUMA 让内存管理从「一视同仁」变成「就近优先」。

# 查看 NUMA 拓扑
numactl --hardware
# 各 node 内存余量
numastat
ls /sys/devices/system/node/node0/ /sys/devices/system/node/node1/

默认策略是 node-local:进程优先从自己所在 node 分配内存。跨 node 访问(memory interleaving)虽然均衡,但带宽与延迟都吃亏:

策略命令场景
local(默认)无绝大多数场景
bind 绑定到指定 nodenumactl --membind=0大内存数据库绑定本地内存
interleave 交错numactl --interleave=all不确定哪个 node 近、或需均衡带宽
# 让某程序跑在 node0 的 CPU 上、内存也绑在 node0
numactl --cpunodebind=0 --membind=0 ./database

记忆:NUMA 调优核心是「进程待在哪,内存就分配在哪」。检查工具看 numastat 的 node_mis hits——本地 miss 过高说明内存分配与 CPU 执行不同 node,改用 --membind 或配 numa_balancing 内核策略。


7. 内存调优参数

内存行为主要由一组 vm.* 内核参数决定,改之前先搞清每个参数在权衡什么:

参数默认作用
vm.swappiness60换出匿名页 vs 回收缓存的倾向
vm.overcommit_memory00=启发式、1=总是允许、2=严格按 CommitLimit
vm.overcommit_ratio50overcommit=2 时允许超过物理内存的比例
vm.vfs_cache_pressure100回收 dentry/inode 缓存的激进程度
vm.dirty_ratio20进程可脏页占内存上限(触发同步写)
vm.dirty_background_ratio10后台写回脏页的水位线
vm.min_free_kbytes自动为原子分配保留的最小空闲内存
# 大内存写缓存型服务器:把脏页写回调宽,减少磁盘 IO 次数
sysctl -w vm.dirty_ratio=30
sysctl -w vm.dirty_background_ratio=5
# 低延迟内存型:overcommit 保持 0(启发式)即可,别开 1
sysctl vm.overcommit_memory
cat /proc/meminfo | grep Commit

铁律:调参前先用监控确认瓶颈在内存,而不是在应用或磁盘。vm.overcommit_memory=1 会让 malloc 永不失败但增加 OOM 风险,不是「内存不够」的正解;正解是加内存或限流(cgroup)。


8. 故障排查:free、ps、vmstat 内存解读

排查内存问题先看三件套:free 看整体水位、vmstat 看回收压力、ps/smem 看谁在吃。

free -h
vmstat 1 5
ps aux --sort=-%mem | head -10
smem -s rss | head -10     # 更精确的「实际占用量」(含共享库拆分)

free 的列很容易误读——buff/cache 是缓存不是「已用」,关键看 available:

free 列含义
total物理内存总量
used真正被进程占用的内存
free完全空闲(不等于可用!)
buff/cache块缓存+页缓存(可回收,别当成泄漏)
available真正可给新程序用的量(≈ free + 可回收缓存)
# vmstat 内存行
# si/so:swap in/out(持续非 0 = 内存吃紧,在换页)
# 非 0 的 si/so 比 free 的 used 更能说明「真压力」
vmstat 1
# 单进程视角
top -o %MEM
# 每秒采样进程 RSS
pidstat -r 1 5

心法:「available 很高 + 没有 si/so」= 内存没问题,别瞎调;「available 持续走低 + si/so 频繁」才是真瓶颈。缓存高不可怕——Linux 用满空闲内存做缓存本来就是设计使然。


9. 实战:内存泄漏与 OOM 排查

一个典型的「内存缓慢爬升最终 OOM」的排查流程:

# 第一步:确认趋势(记录基线,隔 1 小时再看)
free -h > /tmp/mem_baseline.txt
sleep 3600 && free -h | tee /tmp/mem_later.txt
# 第二步:定位哪个进程在涨(对比 RSS)
ps aux --sort=-%mem | head -5
# 第三步:盯住可疑进程的详细内存
cat /proc/<pid>/status | grep -E 'VmRSS|VmSize'
pidstat -r 1 300 > /tmp/pidstat.log

如果进程 RSS 稳定但整机 used 上涨,多半是内核缓存或 slab在涨:

# 区分「进程内存」与「内核内存」
grep -E 'Slab|SReclaimable|SUnreclaim' /proc/meminfo
cat /proc/slabinfo | sort -k3 -rn | head -5
# 文件句柄/连接泄漏也常表现为 slab 上涨
ss -s
# 已经 OOM 了:先看内核判了谁、为什么
dmesg -T | grep -A5 -i 'out of memory'
# 若是容器:看 cgroup 事件
cat /sys/fs/cgroup/<path>/memory.events

记忆:泄漏定位四步 = 看趋势(free)→ 看进程(ps/RSS)→ 看内核(slabinfo)→ 看 OOM 日志(dmesg)。Java/Python 的堆内泄漏要配合 GC/堆转储,本文覆盖的是操作系统层视角。


10. 速查表

需求命令/参数
看整体内存free -h
看回收压力vmstat 1(si/so)
看谁吃内存ps aux --sort=-%mem
精确占用量smem -s rss
进程内存详情cat /proc/<pid>/status(VmRSS)
内核 slabcat /proc/slabinfo
调整 swap 倾向sysctl -w vm.swappiness=10
保护进程不被 OOMecho -1000 > /proc/<pid>/oom_score_adj
cgroup 限内存echo 512M > memory.max
NUMA 信息numactl --hardware / numastat
OOM 日志dmesg -T | grep -i oom
大页配置sysctl -w vm.nr_hugepages=1024

一句话记忆:虚拟内存靠页表,物理不足先回收缓存再换 swap,最后 OOM 止损;排查顺序 free → vmstat → ps/smem → dmesg。调参先确认真瓶颈,别把 overcommit 当内存不足的解药。


延伸阅读

  • /linux-process-management/ — 进程状态与 ps/top 监控
  • /linux-kernel-tuning/ — sysctl 内核参数与调优实践
  • /linux-performance-tuning/ — 性能排查方法论与内存/swap 调优
  • /linux-filesystem-disk/ — 页缓存与磁盘 IO 的读写路径
  • /linux-containers-isolation/ — cgroup 内存限制与容器隔离

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. 《进程调度与 CPU:CFS、优先级与 cgroup CPU 控制》
  2. 《Linux 网络虚拟化:namespace、veth、网桥与虚拟交换机》
  3. 《ELF 二进制与动态链接:从符号到加载》