Skip to content

内存管理

内存管理的功能

  1. 内存分配与回收
  2. 地址转换:逻辑地址 → 物理地址
  3. 内存保护:防止进程越界访问
  4. 内存扩充:虚拟内存技术

内存分配方式

1. 连续分配

单一连续分配

整个内存只有一个用户进程。

┌──────────────┐
│  操作系统    │
├──────────────┤
│  用户进程    │
│              │
└──────────────┘

固定分区分配

将内存划分为固定大小的分区。

┌──────────────┐
│  操作系统    │
├──────────────┤
│  分区1 (2MB) │
├──────────────┤
│  分区2 (4MB) │
├──────────────┤
│  分区3 (8MB) │
└──────────────┘

缺点:内部碎片

动态分区分配

根据进程需求动态分配。

分配算法

首次适应(First Fit)

  • 从头开始找第一个足够大的空闲分区
  • 速度快

最佳适应(Best Fit)

  • 找最小的足够大的空闲分区
  • 减少浪费,但产生小碎片

最坏适应(Worst Fit)

  • 找最大的空闲分区
  • 避免产生太小的碎片

下次适应(Next Fit)

  • 从上次分配位置开始查找

2. 非连续分配

分页存储

将内存和进程地址空间都分成固定大小的页。

逻辑地址空间:     物理内存:
┌───────┐         ┌───────┐
│ 页0   │ ──────> │ 帧5   │
├───────┤         ├───────┤
│ 页1   │ ──────> │ 帧2   │
├───────┤         ├───────┤
│ 页2   │ ──────> │ 帧7   │
└───────┘         └───────┘

页表:记录页号到帧号的映射

页号 | 帧号
-----|-----
  0  |  5
  1  |  2
  2  |  7

地址转换

逻辑地址 = (页号, 页内偏移)
物理地址 = (帧号, 页内偏移)

例如:
页大小 = 4KB = 4096B
逻辑地址 = 8196
页号 = 8196 / 4096 = 2
页内偏移 = 8196 % 4096 = 4
查页表:页2 → 帧7
物理地址 = 7 * 4096 + 4 = 28676

分段存储

按逻辑单位(代码段、数据段、堆、栈)分段。

逻辑地址空间:
┌────────┐
│ 代码段 │ 段0
├────────┤
│ 数据段 │ 段1
├────────┤
│ 堆    │ 段2
├────────┤
│ 栈    │ 段3
└────────┘

段表

段号 | 基址  | 限长
-----|-------|------
  0  | 2000  | 1000
  1  | 5000  | 500
  2  | 8000  | 2000
  3  | 12000 | 1000

地址转换

逻辑地址 = (段号, 段内偏移)
物理地址 = 段基址 + 段内偏移

例如:
段号 = 1, 偏移 = 100
查段表:段1基址 = 5000
物理地址 = 5000 + 100 = 5100

段页式存储

先分段,再分页。

逻辑地址 = (段号, 页号, 页内偏移)

地址转换:
1. 通过段号查段表,得到页表地址
2. 通过页号查页表,得到帧号
3. 物理地址 = 帧号 * 页大小 + 页内偏移

虚拟内存

基本概念

虚拟内存让进程认为自己拥有连续的大内存空间,实际上:

  • 只有部分在物理内存
  • 其余在磁盘(交换区)
虚拟内存:          物理内存:
┌───────┐          ┌───────┐
│ 页0   │ ───────> │ 帧1   │
├───────┤          ├───────┤
│ 页1   │ ───────> │       │
├───────┤  在磁盘   ├───────┤
│ 页2   │          │ 帧0   │
├───────┤          └───────┘
│ 页3   │ ───────> │ 帧2   │
└───────┘          └───────┘

请求分页

缺页中断流程

1. CPU访问某页
2. 检查页表,发现页不在内存(缺页)
3. 触发缺页中断
4. 操作系统从磁盘加载该页到内存
5. 更新页表
6. 重新执行访问指令

页面置换算法

当内存满时,需要选择一个页面换出。

1. 最优置换(OPT)

选择以后最长时间不使用的页面。

理论最优,实际无法实现(无法预知未来)

2. 先进先出(FIFO)

选择最早进入内存的页面。

页面引用串:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
内存帧数:3

1  1  1  4  4  4  5  5  5  3  3  3
   2  2  2  1  1  1  1  1  1  4  4
      3  3  3  2  2  2  2  2  2  2

缺页:✓ ✓ ✓ ✓ ✓ ✓ ✓     ✓ ✓ ✓

缺点:可能出现Belady异常(帧数增加,缺页率反而增加)

3. 最近最少使用(LRU)

选择最长时间未使用的页面。

实现方式:
1. 计数器:每次访问时更新时间戳
2. 栈:访问的页面移到栈顶
3. 链表:访问的页面移到链表头

4. 时钟置换(Clock)

近似LRU的简化算法。

使用位(Use bit):
- 访问页面时置1
- 置换时检查:
  - 0:选中
  - 1:置0,继续查找

   ┌───┬───┬───┬───┐
   │ 1 │ 0 │ 1 │ 1 │
   └─┬─┴─┬─┴─┬─┴─┬─┘
     │   │   │   │
     └───┴───┴───┘
         ↑ 指针

5. 改进的Clock算法

同时考虑使用位和修改位。

(使用位, 修改位)

优先级(低到高):
(0, 0) > (0, 1) > (1, 0) > (1, 1)

页面置换算法对比

算法性能实现复杂度是否实用
OPT最优-不实用
FIFO简单实用
LRU复杂实用
Clock较好简单实用

工作集模型

工作集:进程在某段时间内访问的页面集合。

t时刻的工作集 WS(t, Δ):
在 [t-Δ, t] 时间窗口内访问的页面

驻留集:分配给进程的物理页框集合

  • 太小:频繁缺页
  • 太大:浪费内存

Linux内存管理

内存布局

高地址
┌──────────────┐
│   内核空间    │  1GB
├──────────────┤
│   栈 (↓)     │
│              │
│   ↕          │
│              │
│   堆 (↑)     │
├──────────────┤
│   BSS段      │  未初始化数据
├──────────────┤
│   数据段      │  已初始化数据
├──────────────┤
│   代码段      │  程序代码
└──────────────┘
低地址

内存分配

bash
# 查看进程内存映射
cat /proc/[pid]/maps

# 查看系统内存
free -h
cat /proc/meminfo

# 查看内存使用排行
ps aux --sort=-%mem | head

内存回收

OOM Killer

内存不足时,Linux会选择一个进程杀掉。

bash
# 查看OOM分数
cat /proc/[pid]/oom_score

# 调整OOM分数(-1000到1000)
echo -17 > /proc/[pid]/oom_adj

Swap

交换分区,用于虚拟内存。

bash
# 查看swap
swapon --show
free -h

# 创建swap文件
dd if=/dev/zero of=/swapfile bs=1G count=2
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# 调整swap使用倾向(0-100,值越小越少用swap)
cat /proc/sys/vm/swappiness
echo 10 > /proc/sys/vm/swappiness

内存优化

bash
# 清除缓存
sync
echo 3 > /proc/sys/vm/drop_caches

# 查看slab缓存
slabtop

# 内存调优参数
vm.swappiness          # swap使用倾向
vm.dirty_ratio         # 脏页比例阈值
vm.overcommit_memory   # 内存过量分配策略

内存泄漏检测

C/C++

c
// 使用Valgrind检测
valgrind --leak-check=full ./program

// AddressSanitizer
gcc -fsanitize=address program.c
./a.out

Java

bash
# 堆转储
jmap -dump:format=b,file=heap.bin [pid]

# 分析工具
# - Eclipse MAT
# - VisualVM
# - JProfiler

Linux工具

bash
# 查看进程内存使用
pmap -x [pid]

# 实时监控
top
htop

# 内存分析
smem
vmstat

内存性能优化

1. 减少内存使用

  • 使用合适的数据结构
  • 及时释放不用的内存
  • 对象池、内存池

2. 提高缓存命中率

  • 数据局部性(时间局部性、空间局部性)
  • 减少跨页访问
  • 对齐数据结构

3. 避免内存碎片

  • 使用内存池
  • 定期整理内存
  • 使用合适的分配器

4. NUMA优化

bash
# 查看NUMA信息
numactl --hardware

# 绑定进程到NUMA节点
numactl --cpunodebind=0 --membind=0 ./program

💡 提示

这是一个demo文档,欢迎补充更多内存管理相关内容。

相关链接

基于 VitePress 构建