11.2 内存管理与虚拟内存
一句话
虚拟内存把 “地址” 与 “物理页” 解耦,代价是缺页与 TLB;你看到的 RSS 只是冰山一角——page cache、共享库、匿名页、内核结构都在争同一批物理页,
OOM只是最后一个结果。
地址转换链路
虚拟地址 → TLB 查(命中 ~1ns)→ 未命中查页表(4 级,~10-100ns)
→ 页不存在 → 缺页异常(minor:映射已有页;major:读磁盘,~100μs-10ms)
| 概念 | 含义 | 观察 |
|---|---|---|
| 页大小 | 通常 4KB;大页 2MB / 1GB | getconf PAGE_SIZE |
| 缺页 | minor(映射)/ major(IO) | /proc/vmstat 的 pgmajfault |
| 预读 | 内核按顺序预测多读几页 | readahead |
| 反向映射 | 从物理页找回所有映射它的进程 | 影响回收成本 |
分配器:用户态与内核态各一层
| 层 | 机制 | 关注点 |
|---|---|---|
| 用户态 | brk(小)、mmap(大)、glibc arena | 碎片与多线程争用 |
| 内核态 | buddy + slab/slub | 内存碎片、分配延迟 |
| 语言运行时 | Go 的 mspan/mcache、JVM 的分代 | 见 15.3 内存分配与逃逸分析 |
四个常见指标的含义
| 指标 | 含义 | 误读 |
|---|---|---|
| VSZ | 虚拟地址空间大小 | 大不代表占内存(可能未映射页) |
| RSS | 实际驻留物理页 | 含共享库与 page cache 部分 |
| page cache | 文件缓存 | 可以被回收,不算 “泄漏” |
| swap | 换出的匿名页 | 用上 swap 往往意味着延迟灾难 |
落到数字:缺页与 TLB
| 事件 | 量级 | 触发条件 |
|---|---|---|
| TLB 命中 | ~1ns | 页表项在 TLB 中 |
| TLB 未命中(页表命中) | 10–100ns | 4 级页表遍历 |
| minor fault | 1–10μs | 页已缓存但未映射 |
| major fault | 100μs–10ms | 需要读磁盘/网络存储 |
| 2MB 大页 | 页表项减少 512× | 随机访问密集时收益明显 |
cat /proc/meminfo | grep -E "MemAvailable|Cached|SwapUsed" 2>/dev/null || vm_stat | head -8
grep -E "^(Rss|Pss|Anonymous|Private_Dirty)" /proc/self/smaps_rollup 2>/dev/null || echo "macOS 无 smaps"
反模式
| 反模式 | 后果 |
|---|---|
| 用 RSS 判断泄漏 | page cache 波动被误判为泄漏 |
| 关闭 swap 却按 “有 swap” 规划容量 | 内存压力时直接 OOM Kill |
| 在容器里按宿主机内存设堆上限 | 超过 cgroup 限制被杀(见 6.3 namespace (隔离) 与 cgroups (限制)) |
| 盲目开透明大页 | 分配延迟抖动(THP 的 compaction) |
忽略 GOMEMLIMIT/堆上限 | GC 目标失控,RSS 一路涨 |
三条铁律
🔑 RSS ≠ 内存需求:page cache 与共享库都在里面。
🔑 major page fault 才是磁盘 IO 的信号;minor 只是映射。
🔑 容器内存上限要同时约束 “堆 + 栈 + page cache”,只调堆没用。
自检
Q1 服务的 RSS 从 1GB 涨到 2GB,但 GC 日志显示堆稳定,最可能是什么?
▸ 答案:page cache 或堆外内存:文件读写、mmap、网络缓冲区都会让 RSS 上涨,这部分可回收,不一定是泄漏。区分方法:看 smaps_rollup 的 Pss 与 Anonymous、File-backed 拆分;再看 GC 后的 RSS 是否回落。
Q2 为什么 major page fault 会让延迟出现 “毫秒级尖刺”?
▸ 答案:因为 major fault 需要读磁盘(或网络存储):内核要分配页、发起 IO、等待返回、更新页表,期间进程被阻塞。SSD 上约 100μs,机械盘或网络存储可达毫秒级。预热、预读与内存充足都能减少它。
Q3 VSZ 很大(比如 20GB)是问题吗?
▸ 答案:通常不是。VSZ 包含未实际映射的地址空间(如 mmap 预留、线程栈预留、Go 的堆保留)。判断要看 RSS 与 Anonymous。但如果 VSZ 接近系统 vm.max_map_count 或地址空间上限,会分配失败。
Q4 容器里 JVM/Go 的堆上限该怎么设?
▸ 答案:按 cgroup 限制设,而不是按宿主机内存:JVM 用 -XX:MaxRAMPercentage(配合 UseContainerSupport),Go 用 GOMEMLIMIT(见 15.4 GC 原理与调优 (GOGC / GOMEMLIMIT))。要给 page cache、栈、堆外留 20%–30%,否则 “刚好用满” 时会被 OOM Kill。
Q5 什么时候该用大页(HugePages)?
▸ 答案:大内存 + 随机访问密集的场景(数据库缓冲池、DPDK 网络栈):2MB 页把页表项减少 512 倍、TLB 命中率提升,随机访问延迟下降。代价是分配需预保留、碎片与运维复杂;普通 Web 服务收益不明显。
Q6 为什么 MemAvailable 比 MemFree 更适合判断 “还有多少内存可用”?
▸ 答案:因为 MemFree 只统计完全空闲的页,而 page cache 里的干净页可被立即回收。MemAvailable 已经把 “可回收的缓存与 slab” 算进来,更接近 “新进程还能拿到多少内存”。
关联
- 前置:11.1 进程、线程与调度
- 并列:11.6 CPU Cache 与 NUMA
- 下游:15.3 内存分配与逃逸分析、15.4 GC 原理与调优 (GOGC / GOMEMLIMIT)、6.3 namespace (隔离) 与 cgroups (限制)