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. 联系实际:内存问题怎么排查
看到内存异常,先问:
- 是进程内存高,还是系统内存高?
- 是虚拟内存高,还是常驻物理内存高?
- 堆、栈、mmap、共享内存、外部内存分别占多少?
- 是否线程过多导致栈占用高?
- 是否读大文件导致页缓存增加?
- 是否发生缺页或 swap?
- 是否有资源限制,比如容器限制?
- 是否存在持续增长且不下降的引用?
典型映射:
| 现象 | 方向 |
|---|---|
| 文件第二次读快 | 页缓存命中 |
| 延迟突然尖刺 | 缺页、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 刚被淘汰,又缺页
程序没有做更多计算,但每几步都要等磁盘或慢存储参与换页。于是表现为:
代码块收起展开
CPU 不一定满;
IO 很高;
延迟极不稳定;系统整体像“卡住”。
这就是 thrashing 的第一性解释:活跃工作集大于可用物理内存,系统忙着搬页,而不是推进业务计算。
边界条件:虚拟内存大不等于真的占用物理内存
看到一个进程虚拟地址空间很大,不要立刻判断它“吃掉了这么多内存”。
原因是虚拟内存可以先保留地址范围:
保留一段虚拟地址
还没有真正映射物理页
或映射了文件但尚未读入
真正压力要看:
| 指标/现象 | 更接近的问题 |
|---|---|
| 虚拟地址空间大 | 预留、mmap、地址布局 |
| 常驻内存高 | 实际物理页占用高 |
| 缺页多 | 工作集或访问模式带来加载成本 |
| swap 活跃 | 物理内存压力已经影响运行 |
| OOM | 分配峰值或限制被触发 |
所以内存排障要把“地址空间大小”和“实际物理占用”分开。虚拟内存是地址抽象,物理内存才是稀缺资源。
排障卡:内存增长不等于同一种问题
看到内存升高,先分清是哪类增长:
| 现象 | 更可能的原因 | 先看什么 |
|---|---|---|
| 进程私有内存持续增长 | 对象/缓冲区未释放 | 分配热点、引用链、生命周期 |
| 文件读写后内存升高 | 页缓存增长 | 可回收内存、文件访问模式 |
| 内存接近满且系统变慢 | 换页或内存压力 | 缺页、swap、工作集大小 |
| 子进程创建后内存看似翻倍 | Copy-on-write 视图 | 实际物理页、写入后复制 |
| 突然被杀或分配失败 | OOM 或限制触发 | 内存限制、峰值分配、日志 |
练习:把“内存占用高”拆成“谁占用、能否回收、是否影响性能、是否还在增长”。只有这四问回答清楚,才知道应该优化程序对象、调整缓存策略,还是处理系统内存压力。
13. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 虚拟地址和物理地址有什么区别?
- 页表、MMU、TLB 分别做什么?
- 缺页为什么会让程序突然变慢?
- 栈、堆、mmap 区分别存什么?
- Copy-on-write 为什么能减少复制?
- 页缓存为什么让文件第二次读取更快?
- swap 和 thrashing 为什么会拖垮系统?
- OOM 排查为什么不能只看堆?
内存管理的核心是:OS 用虚拟内存抽象把有限物理内存包装成隔离、可保护、可按需加载的地址空间。