什么是虚拟内存?为什么 8GB 内存能跑几十 GB 的程序
这是“操作系统系列教程”的第十五篇。前面几篇我们讲了进程、线程、调度、同步和死锁,这些都是处理器管理的内容。从这一篇开始,我们进入操作系统另一个核心模块——内存管理。而虚拟内存,是内存管理中最重要、也最精彩的概念。
一个反直觉的现象
先来想一个奇怪的问题。
假设你的电脑只有 8GB 物理内存,但你同时打开了浏览器(占用约 2GB)、IDE(占用约 2GB)、音乐播放器(占用约 500MB)、微信(占用约 1GB)、Photoshop(占用约 3GB),再加上操作系统本身占用的 2GB。
加起来已经超过了 8GB。但电脑并没有崩溃,程序依然能正常运行。这是为什么?
再想一个更极端的例子:你在 Linux 服务器上运行一个需要 10GB 内存的程序,但服务器只有 4GB 物理内存。程序依然能跑起来,只是可能慢一点。
这看起来违背常识:物理内存不够,程序为什么还能运行?
答案就是——虚拟内存(Virtual Memory)。
虚拟内存是操作系统最精妙的设计之一。它让每个进程都以为自己拥有独立、连续、足够大的内存空间,而实际上,这些空间可能并不连续,甚至部分数据根本不在物理内存中。
这篇文章,我们就来把“虚拟内存”这件事讲清楚。
说明:本文涉及地址空间、分页、页表、缺页等概念。初学者如果遇到暂时看不懂的术语,可以先跳过细节,重点理解“虚拟内存让每个进程以为自己独占内存,实际上由操作系统在物理内存和磁盘之间调度”这一核心思想。
一、没有虚拟内存会怎样?
1.1 直接使用物理内存的问题
在早期的计算机中,程序直接使用物理内存地址。这种方式存在几个严重问题:
问题一:地址冲突
如果两个程序都使用物理地址 0x1000,就会互相覆盖。程序 A 写入的数据,可能被程序 B 覆盖,导致程序 A 崩溃。
问题二:内存不足
如果物理内存只有 4GB,而程序需要 6GB,程序就无法运行。程序员必须手动管理内存,把数据分成小块,用到时加载,不用时写回磁盘。这非常繁琐,容易出错。
问题三:安全性差
程序可以直接访问任何物理内存地址,包括其他程序的数据和操作系统的数据。一个恶意程序可以轻易读取或修改其他程序的内容。
问题四:编程困难
程序员必须知道数据在物理内存中的具体位置,无法使用统一的地址空间。程序的可移植性极差。
图 1:直接使用物理内存的四大问题。 地址冲突、内存不足、安全性差、编程困难。
1.2 一个直观的例子
假设物理内存只有 4GB,程序 A 需要 3GB,程序 B 需要 3GB。如果直接使用物理内存,两个程序无法同时运行,因为它们需要 6GB,超过了 4GB。
但如果你观察实际系统,两个 3GB 的程序可以同时运行。这是因为它们并不总是同时使用全部 3GB——程序 A 可能正在等待用户输入,程序 B 可能正在等待网络响应。虚拟内存让操作系统可以在它们之间灵活调度内存。

图 2:虚拟内存让多个程序共享有限的物理内存。 不活跃的数据被换出到磁盘,需要时再换入。
二、虚拟内存到底是什么?
2.1 从生活场景理解
我们可以用“图书馆”来类比虚拟内存。
| 图书馆场景 | 操作系统 |
|---|---|
| 读者 | 进程 |
| 书架上的书 | 物理内存 |
| 图书馆总藏书 | 虚拟内存 |
| 借书证上的编号 | 虚拟地址 |
| 图书管理员 | 内存管理单元(MMU) |
| 书库(不对外开放) | 磁盘 |
读者想借书时,只需要知道书的编号(虚拟地址),不需要知道书具体在哪个书架(物理地址)。如果书在书架上(物理内存中),管理员直接拿给读者;如果书在书库里(磁盘上),管理员先去取来,再交给读者。
读者感觉自己可以借到图书馆的所有书(虚拟内存),但实际上书架(物理内存)只能放下一部分。
2.2 虚拟内存的定义
给出一个正式的定义:
虚拟内存(Virtual Memory)是操作系统提供的一种内存管理机制,它为每个进程提供一个独立、连续、远大于物理内存的虚拟地址空间,并通过地址映射和页面置换,在物理内存和磁盘之间动态调度数据。
这个定义里有四个关键点:
- 独立:每个进程有自己独立的虚拟地址空间,互不干扰;
- 连续:虚拟地址空间是连续的,程序员不需要关心物理内存的碎片;
- 远大于物理内存:虚拟地址空间可以比物理内存大得多;
- 动态调度:操作系统在物理内存和磁盘之间动态换入换出数据。
2.3 虚拟内存的核心思想
虚拟内存的核心思想可以概括为三句话:
- 每个进程都以为自己独占内存;
- 虚拟地址和物理地址分离;
- 只有真正用到的数据才加载到物理内存。
图 3:虚拟内存的核心思想。 独立、分离、按需加载。
三、虚拟地址与物理地址
理解虚拟内存,首先要理解两种地址:虚拟地址和物理地址。
3.1 两种地址的定义
| 地址类型 | 定义 | 谁使用 |
|---|---|---|
| 虚拟地址 | 程序使用的地址,也叫逻辑地址 | 程序员、编译器、应用程序 |
| 物理地址 | 内存单元的实际地址 | 内存管理单元、硬件 |
程序员写代码时,使用的是虚拟地址。CPU 执行指令时,先把虚拟地址转换为物理地址,再访问内存。
3.2 地址转换过程
图 4:地址转换过程。 CPU 发出虚拟地址,MMU 通过页表转换为物理地址。
3.3 一个具体的例子
假设虚拟地址是 32 位,页大小为 4KB。那么:
- 虚拟地址的低 12 位(0~11)是页内偏移;
- 虚拟地址的高 20 位(12~31)是虚拟页号。
地址转换过程:
- 取出虚拟页号(高 20 位);
- 在页表中查找虚拟页号对应的页表项;
- 页表项中存储着物理页框号(如 20 位);
- 把物理页框号和页内偏移拼接,得到物理地址。
例如:
- 虚拟地址:
0x00003A5C; - 虚拟页号:
0x00003; - 页内偏移:
0xA5C; - 页表项查到物理页框号为
0x00012; - 物理地址:
0x00012A5C。
图 5:虚拟地址到物理地址的转换。 虚拟页号通过页表映射为物理页框号,加上页内偏移得到物理地址。
四、分页机制
虚拟内存的实现依赖分页机制。分页把虚拟地址空间和物理内存都划分为固定大小的块,分别称为页和页框。
4.1 页和页框
| 概念 | 位置 | 大小 | 说明 |
|---|---|---|---|
| 页(Page) | 虚拟地址空间 | 4KB(常见) | 虚拟内存的划分单位 |
| 页框(Page Frame) | 物理内存 | 4KB(常见) | 物理内存的划分单位 |
页和页框大小相同,这样虚拟页才能映射到物理页框。
4.2 页表
页表(Page Table)是存储虚拟页到物理页框映射关系的数据结构。每个进程有自己的页表。
页表项通常包含:
| 字段 | 说明 |
|---|---|
| 物理页框号 | 对应的物理页框 |
| 有效位 | 该页是否在物理内存中 |
| 访问位 | 该页是否被访问过 |
| 修改位 | 该页是否被修改过 |
| 保护位 | 读、写、执行权限 |
图 6:页表实现虚拟页到物理页框的映射。 多个虚拟页可以映射到不连续的物理页框。
4.3 多级页表
如果虚拟地址空间很大(如 64 位系统),页表会非常大。例如,48 位虚拟地址、4KB 页大小,页表有 2^36 个页表项,每个页表项 8 字节,总共需要 512GB 来存储页表。这显然不可行。
解决方案是多级页表。把页表本身也分页,只加载用到的部分。
以二级页表为例:
- 顶级页表(页目录)指向二级页表;
- 二级页表指向物理页框。
图 7:二级页表结构。 通过两级索引减少页表占用的内存。
4.4 快表(TLB)
每次访问内存都要查询页表,会降低性能。为了解决这个问题,CPU 引入了快表(Translation Lookaside Buffer,TLB),也叫页表缓存。
TLB 是一个小容量的高速缓存,存储最近使用的页表项。CPU 先查 TLB,如果命中,直接得到物理页框号;如果不命中,再查页表。
图 8:TLB 加速地址转换。 大部分地址转换都能在 TLB 中命中。
五、缺页中断与页面置换
虚拟内存的关键机制是:只有真正用到的页才加载到物理内存。当程序访问一个不在物理内存中的页时,就会触发缺页中断(Page Fault)。
5.1 缺页中断的处理流程
图 9:缺页中断的处理流程。 从磁盘加载缺失的页,更新页表,重新执行指令。
5.2 页面置换算法
如果物理内存已满,需要置换(换出)一些页,腾出空间给新页。选择换出哪个页,就是页面置换算法要解决的问题。
常见的页面置换算法:
| 算法 | 原理 | 优缺点 |
|---|---|---|
| FIFO | 先进先出 | 简单,但可能置换常用页 |
| LRU | 最近最少使用 | 效果好,但实现开销大 |
| Clock | 近似 LRU | 折中方案,实际常用 |
| OPT | 最优置换 | 理论最优,无法实现 |
图 10:常见的页面置换算法。
5.3 LRU 算法示例
假设物理内存有 3 个页框,访问序列为:1, 2, 3, 4, 1, 2, 5, 1, 2, 3。
LRU 置换过程:
| 访问 | 页框状态 | 缺页 |
|---|---|---|
| 1 | 1 | 是 |
| 2 | 1, 2 | 是 |
| 3 | 1, 2, 3 | 是 |
| 4 | 4, 2, 3(换出 1) | 是 |
| 1 | 4, 1, 3(换出 2) | 是 |
| 2 | 4, 1, 2(换出 3) | 是 |
| 5 | 5, 1, 2(换出 4) | 是 |
| 1 | 5, 1, 2(命中) | 否 |
| 2 | 5, 1, 2(命中) | 否 |
| 3 | 5, 3, 2(换出 1) | 是 |
总共缺页 8 次。
图 11:LRU 页面置换示例。 访问序列和缺页情况。
六、虚拟内存的优势
6.1 突破物理内存限制
虚拟内存让程序可以使用比物理内存更大的地址空间。不活跃的页被换出到磁盘,需要时再换入。这就是为什么 8GB 内存能跑几十 GB 的程序。
6.2 地址空间隔离
每个进程有独立的虚拟地址空间,进程之间不能直接访问对方的内存。这提高了系统的安全性。
6.3 简化编程
程序员不需要关心物理内存的分配和碎片,只需要使用虚拟地址。编译器、链接器都可以基于虚拟地址工作。
6.4 内存共享
多个进程可以共享同一块物理内存,例如共享库。多个进程的虚拟页可以映射到同一个物理页框。
图 12:虚拟内存的四大优势。
七、虚拟内存的代价
虚拟内存不是免费的,它也有代价。
7.1 地址转换开销
每次访问内存都要进行地址转换。虽然有 TLB 加速,但仍然有开销。
7.2 缺页中断开销
缺页中断需要访问磁盘,磁盘速度比内存慢几个数量级。频繁的缺页中断会导致系统性能急剧下降。
7.3 页表占用内存
页表本身占用内存。多级页表减少了占用,但仍然需要一定的空间。
7.4 抖动
如果物理内存严重不足,系统会频繁换入换出,导致 CPU 大部分时间花在换页上,而不是执行程序。这种现象称为抖动(Thrashing)。

图 13:虚拟内存的代价。 抖动是最严重的性能问题。
八、虚拟内存的实际应用
8.1 Linux 中的虚拟内存
Linux 使用分页机制实现虚拟内存。每个进程有独立的页表,内核维护物理页框的分配和回收。
常用命令:
# 查看内存使用情况
free -h
# 查看虚拟内存统计
vmstat
# 查看进程内存映射
pmap PID
8.2 Windows 中的虚拟内存
Windows 使用页面文件(Page File)实现虚拟内存。页面文件通常位于 C:\pagefile.sys,大小可以配置。
8.3 交换空间
Linux 使用交换空间(Swap)作为虚拟内存的磁盘部分。交换空间可以是分区,也可以是文件。
# 查看交换空间
swapon --show
# 创建交换文件
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
图 14:不同操作系统的虚拟内存实现。
九、常见误区澄清
| 误区 | 真相 |
|---|---|
| “虚拟内存就是磁盘上的交换文件” | 虚拟内存是地址空间抽象,交换文件只是它的磁盘部分 |
| “虚拟内存越大越好” | 交换空间过大会导致频繁换页,性能下降 |
| “有了虚拟内存就不需要物理内存” | 物理内存仍然是性能的关键,虚拟内存只是补充 |
| “虚拟地址就是物理地址” | 两者完全不同,通过页表映射 |
| “页越大越好” | 页越大,内部碎片越多;页越小,页表越大 |
| “缺页中断就是错误” | 缺页中断是正常机制,不是错误 |
| “虚拟内存只用于内存不足时” | 虚拟内存始终在工作,即使内存充足 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)