Q1:为什么要有虚拟内存?
A1:
什么是虚拟内存?
形象类比:酒店管理系统
物理内存(RAM) = 酒店的真实房间(有限、分散、宝贵)
虚拟内存 = 房号卡片(连续、独立、无限感)
客人(进程)入住:
• 客人以为自己是 101, 102, 103... 连续房间(虚拟地址连续)
• 实际经理(OS)安排的是 301, 805, 202... 分散房间(物理地址分散)
• 客人不需要知道真实房间号,只管用房号刷卡(MMU 地址翻译)
• 房间不够时,经理把行李暂存仓库(磁盘 Swap)
定义
虚拟内存 是操作系统提供的一种内存管理技术 。它为每个进程创建一个独立的、连续的虚拟地址空间,并将其映射到物理内存或磁盘上。
📦 进程视角(虚拟地址空间)
┌─────────────────┐
│ 0x00000000 │
│ ... │
│ 堆 (Heap) │ ← Java 对象在这里
│ ... │
│ 栈 (Stack) │ ← 线程栈在这里
│ ... │
│ 0xFFFFFFFF │
└─────────────────┘
↕ 映射 (MMU)
📦 物理视角(物理内存 + 磁盘)
┌───────────┐ ┌───────────┐
│ 物理页框 1 │ │ 磁盘 Swap │
│ 物理页框 2 │ │ 分区 │
│ 物理页框 3 │ └───────────┘
└───────────┘
五大核心理由
1️⃣ 内存隔离与安全(Isolation)
问题 :如果没有虚拟内存,进程直接访问物理地址。进程 A 可能 覆盖进程 B 的数据(比如 MySQL 的数据被 Tomcat 改了)。 解决 :每个进程有独立的虚拟地址空间。进程 A 的 0x1000 和进程 B 的 0x1000 指向不同的物理内存。 Java 场景:
- 同一个服务器上跑 10 个 JVM 进程,互不干扰。
- 一个进程崩溃(段错误),不会影响其他进程。
2️⃣ 简化编程模型(Continuity)
问题 :物理内存是碎片化的。如果程序需要 100MB 连续内存,物理上可能找不到这么大的连续空闲块。 解决 :虚拟内存对进程呈现连续地址 。操作系统通过页表将虚拟的连续地址映射到物理的分散页框。 Java 场景:
new byte[100 * 1024 * 1024]永远能分配到连续虚拟地址,无需关心物理碎片。- JVM 堆内存逻辑上是连续的,物理上可以是分散的。
3️⃣ 内存超卖(Overcommitment)
问题 :物理内存昂贵且有限(比如只有 16GB)。 解决 :虚拟内存允许进程申请的内存总和 超过 物理内存大小。未使用的内存可以不分配物理页,或者换出到磁盘。 Java 场景:
- 服务器 16GB 内存,可以启动 5 个
-Xmx4GB的 JVM 进程(总虚拟 20GB > 物理 16GB)。 - 前提:这些进程不会同时达到内存峰值,否则会触发 Swap 或 OOM Killer。
4️⃣ 按需加载(Demand Paging)
问题 :程序很大(比如 1GB),但启动时只用了一小部分。全部加载到内存太浪费。 解决 :只有当进程真正访问某个页面时,操作系统才分配物理内存并加载数据。未访问的代码/数据可以留在磁盘。 Java 场景:
- Java 类加载:类文件在磁盘,用到时才加载到内存(方法区)。
- 大文件处理:
MappedByteBuffer映射 1GB 文件,但只消耗访问部分的物理内存。
5️⃣ 共享内存(Shared Memory)🤝
问题 :多个进程需要共享数据(如动态库、IPC)。 解决 :不同进程的虚拟地址可以映射到同一个物理页框 。 Java 场景:
- 多个 JVM 进程共享相同的 JDK 类库代码段(节省物理内存)。
- 之前讲的 IPC 共享内存/mmap 底层就是利用虚拟内存映射同一物理页。
核心机制:它是如何工作的?
1. 页表(Page Table)
操作系统维护的一张地图,记录 虚拟页号 → 物理页框号 的映射。
虚拟地址:[ 页号 100 | 偏移量 0x123 ]
↓ 查页表
物理地址:[ 页框 500 | 偏移量 0x123 ]
2. MMU(内存管理单元)
CPU 中的硬件组件,负责自动进行地址翻译。对软件透明。
- 每次内存访问都要经过 MMU。
- 性能开销:查页表需要访问内存,会拖慢 CPU。
3. TLB(快表)
Translation Lookaside Buffer,CPU 缓存中的页表缓存。
- 存储最近使用的虚拟→物理映射。
- 命中 TLB:极速(1 个 CPU 周期)。
- 未命中 TLB:慢(需查内存页表)。
- Java 优化 :使用 Huge Pages(大页) 可以减少页表项,提高 TLB 命中率,提升大堆内存性能。
4. 缺页中断(Page Fault)
当访问的虚拟页不在物理内存中时:
- CPU 触发缺页中断。
- 操作系统暂停进程,从磁盘加载数据到物理内存。
- 更新页表,恢复进程执行。
- 硬缺页:需读磁盘(慢,毫秒级)。
- 软缺页:只需更新页表(快,微秒级)。
Q2:什么是分段?什么是分页?
A2:
核心概念对比
| 维度 | 分段 (Segmentation) | 分页 (Paging) |
|---|---|---|
| 划分依据 | 逻辑(程序结构) | 物理(内存效率) |
| 大小 | 可变(由程序决定) | 固定(由系统决定,如 4KB) |
| 地址空间 | 二维(段号 + 段内偏移) | 一维(页号 + 页内偏移) |
| 碎片问题 | 外部碎片(内存空洞) | 内部碎片(页内浪费) |
| 主要目的 | 方便编程、保护、共享 | 提高内存利用率、简化管理 |
| 现代应用 | 较少(x86-64 基本废弃) | 主流(Linux/Windows 核心机制) |
分段(Segmentation)
1. 什么是分段?
按照程序的逻辑结构划分内存。每个段代表一个有意义的逻辑单元。
📦 程序逻辑结构
┌─────────────┐
│ 代码段 │ (Code) ← 只读
├─────────────┤
│ 数据段 │ (Data) ← 全局变量
├─────────────┤
│ 堆段 │ (Heap) ← 动态分配 (new Object)
├─────────────┤
│ 栈段 │ (Stack) ← 局部变量
└─────────────┘
2. 地址翻译机制
逻辑地址 = 段号 + 段内偏移
CPU 生成地址:[ 段号 2 | 偏移 0x100 ]
↓ 查段表 (Segment Table)
物理地址:[ 段 2 的基址 0x5000 | 偏移 0x100 ] = 0x5100
3. 优点
- 符合人类思维:程序员知道代码、数据、栈是分开的。
- 便于保护:代码段设为只读,栈段设为不可执行(防溢出攻击)。
- 便于共享 :多个进程可以共享同一个代码段(如动态库
.so)。
4. 缺点
- 外部碎片(External Fragmentation) :
- 段长度可变,内存中会留下许多无法利用的小空洞。
- 需要复杂的内存紧缩(Compaction) 算法来整理碎片。
- 内存分配慢:需要寻找足够大的连续空闲块。
分页(Paging)
1. 什么是分页?
将物理内存和虚拟内存都切成固定大小的块。
-
页(Page):虚拟内存块(通常 4KB)。
-
页框(Page Frame):物理内存块(通常 4KB)。
📦 虚拟内存 📦 物理内存
┌─────────┐ ┌─────────┐
│ 页 0 │ │ 页框 5 │ ← 页 0 映射到这里
├─────────┤ ├─────────┤
│ 页 1 │ │ 页框 2 │ ← 页 1 映射到这里
├─────────┤ ├─────────┤
│ 页 2 │ │ 页框 8 │ ← 页 2 映射到这里
└─────────┘ └─────────┘
↑ ↑
逻辑连续 物理分散
2. 地址翻译机制
逻辑地址 = 页号 + 页内偏移
CPU 生成地址:[ 页号 100 | 偏移 0x123 ]
↓ 查页表 (Page Table)
物理地址:[ 页框 500 | 偏移 0x123 ]
关键:页内偏移直接复制,只需翻译页号到页框号。
3. 优点
- 无外部碎片:任何空闲页框都可以分配给任何页。
- 内存利用率高:无需连续物理内存。
- 管理简单:固定大小,易于操作系统调度。
4. 缺点
- 内部碎片(Internal Fragmentation) :
- 进程最后一页可能填不满(例如需要 4KB+1B,分配 2 页,浪费近 4KB)。
- 平均浪费半页空间。
- 无逻辑意义:页是物理概念,无法直接体现代码/数据结构。
- 页表开销:需要额外的内存存储页表(通过多级页表优化)。
四、段页式(Segmented Paging)🔄
现代操作系统(如 x86 Linux)通常结合两者优点:先分段,再分页。
逻辑地址 → [ 段机制 ] → 线性地址 → [ 页机制 ] → 物理地址
- 分段:将程序分成段(代码、数据等),提供逻辑保护和共享。
- 分页:将每个段再分成页,解决外部碎片问题。
⚠️ 注意 :在 x86-64 Linux 模式下,分段功能被极大弱化(Flat Memory Model)。
- 所有段的基址都设为 0,限长设为最大。
- 实际上主要靠分页机制管理内存。
- 所以 Linux 开发者通常只关心"分页"。