引言
进程看到的是连续、独立、巨大的虚拟地址空间,底层却共享有限的物理内存——Linux 靠分页、页表、回收与交换在这两层之间搭桥。内存不足时内核先回收缓存、再换出匿名页(swap),实在撑不住就交给 OOM killer 杀进程止损。本文按「机制 → 参数 → 排查」的路径讲透内存管理:从虚拟内存与分页、页表与 HugePages、slab 内核缓存,到 swap 与 OOM、NUMA 与 cgroup 限制,最后用 free/ps/vmstat/smem 实战定位内存泄漏。
前置:/linux-process-management/(进程与 /proc 监控基础)。内核参数调优参考 /linux-kernel-tuning/。
目录
- 1. 虚拟内存与分页机制
- 2. 页表与 HugePages
- 3. slab 与内存碎片
- 4. swap 交换与内存回收
- 5. OOM killer 与 cgroup 内存限制
- 6. NUMA 架构与内存策略
- 7. 内存调优参数
- 8. 故障排查:free、ps、vmstat 内存解读
- 9. 实战:内存泄漏与 OOM 排查
- 10. 速查表
- 延伸阅读
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 多 |
| 2MB | HugeTLB/THP | TLB 覆盖范围大 512 倍,适合大内存应用 |
| 1GB | HugeTLB | 数据库/虚拟化大内存场景,启动前预留 |
记忆:数据库、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 绑定到指定 node | numactl --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.swappiness | 60 | 换出匿名页 vs 回收缓存的倾向 |
| vm.overcommit_memory | 0 | 0=启发式、1=总是允许、2=严格按 CommitLimit |
| vm.overcommit_ratio | 50 | overcommit=2 时允许超过物理内存的比例 |
| vm.vfs_cache_pressure | 100 | 回收 dentry/inode 缓存的激进程度 |
| vm.dirty_ratio | 20 | 进程可脏页占内存上限(触发同步写) |
| vm.dirty_background_ratio | 10 | 后台写回脏页的水位线 |
| 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) |
| 内核 slab | cat /proc/slabinfo |
| 调整 swap 倾向 | sysctl -w vm.swappiness=10 |
| 保护进程不被 OOM | echo -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 内存限制与容器隔离
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。