虚拟内存与分页机制?

2026年 阅读约 undefined 分钟 undefined

虚拟内存为什么让每个进程都觉得自己独占 4GB?本文讲清分页机制、页表与 TLB、缺页中断、页面置换算法(LRU/CLOCK)与 OOM 处理,对比段式与段页式,附 Linux 内存排查命令与面试高频问答。

一句话总结

虚拟内存让每个进程都以为自己独占整个地址空间(64 位下理论 128TB),实际物理内存只有几个 GB。做法是把地址空间切成固定大小的页(4KB),通过页表把虚拟页映射到物理页框;访问的页不在内存就触发缺页中断,由内核从磁盘换入。内存不够时按页面置换算法(LRU、CLOCK)淘汰旧页,淘汰太多导致抖动,最终可能触发 OOM。

初级理解

没有虚拟内存会怎样

# 直接使用物理地址的问题: # 1. 进程能访问任意内存,一个野指针就能让整台机器崩溃 # 2. 两个程序的地址空间互相重叠,无法同时运行 # 3. 程序大小受物理内存限制,16GB 内存跑不了 32GB 的程序 # 虚拟内存解决: # 1. 隔离:每个进程独立地址空间,互相看不见 # 2. 共享:父子进程按需共享页(fork 的 COW 就靠这个) # 3. 扩容:不常用的页放磁盘,程序可以比内存大

分页:把地址空间切成页

# 虚拟地址(32 位)拆成两段: # | 虚拟页号 VPN(20 位) | 页内偏移 offset(12 位) | # 4KB 一页 → 2^20 = 100 万个虚拟页 # 地址转换过程: # 1. CPU 拿到虚拟地址,拆出 VPN 和 offset # 2. 查页表:VPN → 物理页框号 PFN(命中 TLB 时更快) # 3. 拼出物理地址:PFN + offset # 4. 页表项标记"不在内存"→ 触发缺页中断,内核调页

页表存在哪

# 每个进程一套页表,存放在内核空间(物理内存) # 32 位单级页表要 4MB/进程,100 个进程就 400MB,不可接受 # → 多级页表:一级页表 1024 项,只有用到的二级页表才分配 # 64 位用四级页表(PGD/PUD/PMD/PTE),按需分配,开销可控

中级深入

TLB:地址转换的缓存

# 每次访存都查页表 = 每访问一次数据多一次内存访问,性能腰斩 # TLB(Translation Lookaside Buffer)缓存最近用过的 VPN→PFN # 命中率通常 99%+,命中时零额外访存 # 上下文切换要刷新 TLB(地址空间变了) # 优化:PCID/ASID 给 TLB 条目打标签,切换后不必全刷 # 大页(HugePage 2MB/1GB):一个 TLB 条目覆盖更多内存,减少 TLB 压力

缺页中断的处理流程

# 1. CPU 访问虚拟地址,MMU 查页表发现有效位为 0 # 2. 触发缺页中断(Page Fault),陷入内核 # 3. 内核判断:是合法的缺页(该页在交换区/文件中)还是非法访问? # - 合法:找到或分配一个空闲物理页框,从磁盘/文件读入,更新页表 # - 非法:SIGSEGV,段错误(Java 里就是 OOM 的前兆) # 4. 没有空闲页框 → 按置换算法选一页淘汰: # - 干净的页直接丢弃,脏的页先写回磁盘(swap) # 5. 恢复中断现场,重新执行那条指令

页面置换算法

算法思路优缺点
FIFO淘汰最早进入的页简单,可能淘汰常用页,有 Belady 异常
LRU淘汰最久未使用的页效果好,需硬件支持或近似实现
CLOCK环形链表 + 引用位,近似 LRULinux 实际采用的改进版
LFU淘汰使用次数最少的页适应频率,但对历史流量敏感

LRU 理论上最优但精确实现代价高,操作系统普遍用 CLOCK 及其变种(如 Linux 的活跃/非活跃链表)做近似。

高级进阶

段式与段页式

# 段式:按逻辑单元(代码段、数据段、栈段)划分,大小不等 # 优点:符合程序结构、便于共享与保护;缺点:易产生外部碎片 # 页式:固定大小,无外部碎片;缺点:页与逻辑结构无关 # 段页式:先分段再分页,兼得两者好处,x86 就是段页式(64 位后段基本退化)

写时复制(COW)

# fork 时父子进程共享所有页,标记为只读 # 任一方写入 → 触发缺页中断 → 内核复制这一页给写入方 # 好处:fork 后立刻 exec 的场景几乎零复制成本 # Linux 的 fork 快、创建线程快,都受益于 COW

抖动与 OOM

# 抖动(thrashing):分配的页框太少,刚换入的页马上又被换出 # CPU 大量时间花在缺页中断上,系统吞吐暴跌 # 解法:降低多道程序度、用工作集模型分配页框、换更大的内存 # OOM:连置换都救不回来时内核的最后一招 # Linux OOM Killer 按 oom_score 挑一个进程杀掉(分高的是吃内存多的) # 排查:dmesg 看 "Out of memory: Killed process"

实战场景

# 场景1:Linux 看内存与缺页 free -h # 看总量/可用/buffer/cache cat /proc/meminfo # 详细:AnonPages、SwapCached、PageTables vmstat 1 # si/so 列:每秒换入/换出的页(长期非 0 就是在抖) ps -o min_flt,maj_flt,cmd -p PID # 次缺页(内存内解决)/主缺页(要走磁盘)
# 场景2:Java 内存问题定位 # Xmx 设得太小 + 堆内对象过多 → java.lang.OutOfMemoryError # 排查:jstat -gcutil 看各区占用 → jmap -dump 拿堆 → MAT 分析 # 经验:留足容器内存(Xmx 别超过容器 limit 的 70%,给堆外和内核留空间)
# 场景3:该加内存还是该优化 # vmstat 的 si/so 持续很高 + CPU 等 IO → 内存不足,加内存或降并发 # si/so 接近 0 但应用慢 → 不是内存问题,查 CPU/锁/慢 SQL # 大页适用场景:数据库(Oracle/Redis)减少 TLB miss,需预留内存

面试模拟

面试官:为什么需要虚拟内存?

你:三个原因:隔离(每个进程独立地址空间,互相不会破坏)、共享(按需共享页面,fork 的写时复制靠它)、扩容(不常用的页放磁盘,程序可以超过物理内存)。代价是每次访存要做地址转换,所以用 TLB 缓存。

面试官:缺页中断发生了什么事?

你:CPU 访问的虚拟页不在物理内存,MMU 触发缺页中断陷入内核。内核先判断是否合法:合法就从交换区或文件把这个页读进来、更新页表,没有空闲页框就先按置换算法淘汰一页;非法访问就直接段错误。处理完恢复现场重新执行指令。

面试官:LRU 为什么难以精确实现?

你:精确 LRU 要记录每个页最后一次访问的时间并在淘汰时排序,每次访存都更新开销太大,硬件不支持。所以工程上用近似:CLOCK 给每页一个引用位,扫描时优先淘汰引用位为 0 的页并顺手清零,一轮扫完就近似了 LRU,Linux 用的是更细的活跃/非活跃链表。