memory - management

03. 内存管理

0. 本章先解决什么问题

内存管理要解决:

多个进程如何共享有限物理内存,又互不破坏?

应用程序看到的是连续的虚拟地址空间,但机器真实内存是有限、分块、会被多个进程竞争的资源。

本章要讲清:

  • 虚拟地址和物理地址的区别。
  • 页、页表、MMU、TLB 如何协作。
  • 缺页、页缓存、swap 为什么会造成延迟抖动。
  • 堆、栈、映射区分别是什么。
  • OOM、内存泄漏、内存碎片、页缓存压力如何排查。

虚拟地址到物理地址

这张图怎么读

这张图先帮你建立边界:应用看到虚拟地址,硬件通过 TLB/MMU 和页表把它翻译到物理内存;如果页不在内存里,OS 介入处理缺页。内存管理的性能和可靠性问题,大多就发生在这条翻译路径和缺页路径上。

1. 为什么需要虚拟内存

如果程序直接使用物理地址,会有很多问题:

  • 进程之间容易互相破坏。
  • 程序必须关心自己被放在物理内存哪里。
  • 内存碎片会让分配变困难。
  • 共享和保护很难做。
  • 程序无法简单地按需加载大文件或大地址空间。

虚拟内存让每个进程看到自己的地址空间:

进程 A: 0x1000
进程 B: 0x1000

它们的虚拟地址可以相同,但映射到不同物理页。

2. 虚拟地址到物理地址

转换流程:

虚拟地址
-> MMU 查页表
-> 得到物理页框
-> 加上页内偏移
-> 物理地址

虚拟地址通常分成:

虚拟页号 + 页内偏移

页表记录虚拟页到物理页框的映射。

手推一次地址翻译

假设页大小是 4 KB,也就是 $2^{12}$ bytes。一个虚拟地址可以拆成:

  • 高位:虚拟页号 VPN
  • 低 12 位:页内偏移 offset

如果虚拟地址是:

0x12345

那么:

offset = 0x345
VPN = 0x12

页表里如果记录:

VPN 0x12 -> physical frame 0xAB

物理地址就是:

0xAB000 + 0x345 = 0xAB345

关键事实是:地址翻译改变的是“虚拟页号对应哪个物理页框”,页内偏移不变。理解这一点,后面看 page fault、TLB、mmap、copy-on-write 都会更稳。

3. 页和页框

概念含义
page虚拟内存中的固定大小块
frame物理内存中的固定大小块
page table虚拟页到物理页框的映射表

分页的好处:

  • 不要求进程内存物理连续。
  • 方便权限控制。
  • 方便按需加载。
  • 方便共享页面。
  • 减少外部碎片。

代价:

  • 页表占内存。
  • 地址翻译有成本。
  • 缺页处理很贵。

4. 页表项保存什么

页表项可能包含:

信息作用
有效位这个虚拟页是否有效
物理页框号映射到哪个物理页
权限可读、可写、可执行
present是否在物理内存
dirty是否被修改
accessed是否被访问过
copy-on-write是否写时复制

如果访问权限不对,可能触发异常。

例如:

  • 写只读页。
  • 执行不可执行页。
  • 访问无效地址。

5. TLB:页表查询的缓存

如果每次内存访问都查页表,成本很高。

TLB 是地址翻译缓存:

虚拟页 -> 物理页框

流程:

虚拟地址
-> 查 TLB
-> 命中: 快速得到物理页
-> 未命中: 查页表,再填入 TLB

TLB miss 会增加访问成本。

上下文切换、地址空间变化、大工作集都可能影响 TLB 命中率。

6. 缺页异常

程序访问某个虚拟页,但页表显示它当前不在物理内存中,会发生缺页。

流程:

访问虚拟地址
-> MMU 发现页不在内存
-> 触发缺页异常
-> OS 接管
-> 找空闲页框
-> 从文件或交换区加载页面
-> 更新页表和 TLB
-> 程序继续执行

缺页可能是正常的按需加载,也可能是内存压力导致频繁换页。

缺页成本高,因为可能涉及磁盘 IO。

7. 栈、堆、映射区

进程地址空间里常见区域:

区域用途
代码段程序指令
数据段全局或静态数据
动态分配对象
函数调用、局部变量、返回地址
mmap 区映射文件、共享库、共享内存

每个线程通常有自己的栈。

堆通常由运行时或内存分配器管理。

mmap 可以把文件或匿名内存映射到进程地址空间。

8. Copy-on-write

写时复制是一种延迟复制策略。

两个进程先共享同一物理页:

进程 A -> 页面 X
进程 B -> 页面 X

只要都读,就不用复制。

当某个进程写入:

复制一份页面
写入自己的副本

好处:

  • 减少不必要复制。
  • 加快进程创建。
  • 节省内存。

代价:

  • 第一次写入会触发缺页和复制成本。

Copy-on-write 的反例:读多写少才赚

COW 的收益来自“很多页共享后最终没有被写”。如果大量页面很快都被写,COW 反而会把成本推迟到写入时爆发:

场景COW 表现
创建子进程后马上执行新程序很多旧页不需要复制,收益很大
父子进程大量读取同一数据共享页,节省内存
子进程立刻修改大量内存频繁缺页和复制,延迟集中出现
大对象被多个进程共享但偶尔写要关注首次写入抖动

所以看到“创建很快,但第一次写很慢”,要想到 COW 可能参与了路径。COW 不是免费复制,它是把复制推迟到第一次写入。

9. 页缓存

OS 会把文件内容缓存到内存中,叫页缓存。

读文件:

命中页缓存 -> 从内存返回
未命中 -> 从磁盘读入页缓存

写文件:

可能先写页缓存
之后异步刷盘

所以:

写返回成功
不一定等于数据已经落到持久介质

需要持久性时,要关注同步刷盘、日志和文件系统语义。

10. swap 和抖动

物理内存不够时,OS 可以把部分页换到磁盘。

如果频繁换入换出:

程序访问页 A -> 从磁盘换入
又访问页 B -> A 被换出
再访问 A -> 又换入

系统会严重变慢。

这叫 thrashing。

现象:

  • CPU 利用率可能不高。
  • 磁盘 IO 很高。
  • 程序延迟极不稳定。
  • 整机卡顿。

11. OOM 和内存泄漏

OOM 表示内存不够或超过限制。

但“内存”不只堆:

  • 堆对象。
  • 线程栈。
  • mmap 区。
  • 共享内存。
  • 页缓存。
  • 外部/native 内存。
  • 内核对象。
  • 容器或资源限制。

内存泄漏是:

不再需要的数据仍然被引用或占用
导致无法释放

不同语言和运行时表现不同,但 OS 层要看进程整体内存、虚拟内存、常驻内存、映射区等。

12. 联系实际:内存问题怎么排查

看到内存异常,先问:

  1. 是进程内存高,还是系统内存高?
  2. 是虚拟内存高,还是常驻物理内存高?
  3. 堆、栈、mmap、共享内存、外部内存分别占多少?
  4. 是否线程过多导致栈占用高?
  5. 是否读大文件导致页缓存增加?
  6. 是否发生缺页或 swap?
  7. 是否有资源限制,比如容器限制?
  8. 是否存在持续增长且不下降的引用?

典型映射:

现象方向
文件第二次读快页缓存命中
延迟突然尖刺缺页、swap、页缓存抖动
进程被杀OOM 或资源限制
线程很多内存高每线程栈和调度资源
虚拟内存很大但常驻不大mmap 或预留地址空间

手推:工作集超过物理内存为什么会抖动

假设一个程序循环访问 5 个页:

A B C D E A B C D E …

但物理内存只能容纳 3 个页。访问过程可能变成:

访问 A -> 缺页,装入 A
访问 B -> 缺页,装入 B
访问 C -> 缺页,装入 C
访问 D -> 缺页,淘汰 A,装入 D
访问 E -> 缺页,淘汰 B,装入 E
访问 A -> A 刚被淘汰,又缺页

程序没有做更多计算,但每几步都要等磁盘或慢存储参与换页。于是表现为:

代码块JAVA · 3 行收起展开
CPU 不一定满;
IO 很高;
延迟极不稳定;

系统整体像“卡住”。

这就是 thrashing 的第一性解释:活跃工作集大于可用物理内存,系统忙着搬页,而不是推进业务计算。

边界条件:虚拟内存大不等于真的占用物理内存

看到一个进程虚拟地址空间很大,不要立刻判断它“吃掉了这么多内存”。

原因是虚拟内存可以先保留地址范围:

保留一段虚拟地址
还没有真正映射物理页
或映射了文件但尚未读入

真正压力要看:

指标/现象更接近的问题
虚拟地址空间大预留、mmap、地址布局
常驻内存高实际物理页占用高
缺页多工作集或访问模式带来加载成本
swap 活跃物理内存压力已经影响运行
OOM分配峰值或限制被触发

所以内存排障要把“地址空间大小”和“实际物理占用”分开。虚拟内存是地址抽象,物理内存才是稀缺资源。

排障卡:内存增长不等于同一种问题

看到内存升高,先分清是哪类增长:

现象更可能的原因先看什么
进程私有内存持续增长对象/缓冲区未释放分配热点、引用链、生命周期
文件读写后内存升高页缓存增长可回收内存、文件访问模式
内存接近满且系统变慢换页或内存压力缺页、swap、工作集大小
子进程创建后内存看似翻倍Copy-on-write 视图实际物理页、写入后复制
突然被杀或分配失败OOM 或限制触发内存限制、峰值分配、日志

练习:把“内存占用高”拆成“谁占用、能否回收、是否影响性能、是否还在增长”。只有这四问回答清楚,才知道应该优化程序对象、调整缓存策略,还是处理系统内存压力。

13. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 虚拟地址和物理地址有什么区别?
  2. 页表、MMU、TLB 分别做什么?
  3. 缺页为什么会让程序突然变慢?
  4. 栈、堆、mmap 区分别存什么?
  5. Copy-on-write 为什么能减少复制?
  6. 页缓存为什么让文件第二次读取更快?
  7. swap 和 thrashing 为什么会拖垮系统?
  8. OOM 排查为什么不能只看堆?

内存管理的核心是:OS 用虚拟内存抽象把有限物理内存包装成隔离、可保护、可按需加载的地址空间。

延伸阅读