从零起步学习计算机操作系统:内存管理篇

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)

当访问的虚拟页不在物理内存中时:

  1. CPU 触发缺页中断。
  2. 操作系统暂停进程,从磁盘加载数据到物理内存。
  3. 更新页表,恢复进程执行。
  • 硬缺页:需读磁盘(慢,毫秒级)。
  • 软缺页:只需更新页表(快,微秒级)。

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)通常结合两者优点:先分段,再分页

复制代码
逻辑地址 → [ 段机制 ] → 线性地址 → [ 页机制 ] → 物理地址
  1. 分段:将程序分成段(代码、数据等),提供逻辑保护和共享。
  2. 分页:将每个段再分成页,解决外部碎片问题。

⚠️ 注意 :在 x86-64 Linux 模式下,分段功能被极大弱化(Flat Memory Model)。

  • 所有段的基址都设为 0,限长设为最大。
  • 实际上主要靠分页机制管理内存
  • 所以 Linux 开发者通常只关心"分页"。
相关推荐
计算机学姐1 小时前
基于SpringBoot的中药材店铺管理系统
java·vue.js·spring boot·后端·spring·tomcat·推荐算法
青柠代码录1 小时前
【MySQL】事务:如何使用事务
后端
夏日听雨眠2 小时前
文件学习终
windows·学习
开开心心就好2 小时前
轻量级PDF阅读器,仅几M大小打开秒开
linux·运维·服务器·安全·pdf·1024程序员节·oneflow
ljh5746491192 小时前
linux sed 命令
linux·运维·服务器
wbs_scy2 小时前
Linux 进程间通信之管道基础解析 —— 匿名管道的原理与实现
linux·运维·服务器
就叫你天选之人啦2 小时前
GBDT系列八股(XGBoost、LightGBM)
人工智能·深度学习·学习·机器学习
IMPYLH2 小时前
Linux 的 basename 命令
linux·运维·服务器·ssh·bash
yinyan13142 小时前
一起学springAI系列一:使用多种聊天模型
java·人工智能·spring boot·后端·spring·springai