内存管理
内存管理的功能
- 内存分配与回收
- 地址转换:逻辑地址 → 物理地址
- 内存保护:防止进程越界访问
- 内存扩充:虚拟内存技术
内存分配方式
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_adjSwap
交换分区,用于虚拟内存。
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.outJava
bash
# 堆转储
jmap -dump:format=b,file=heap.bin [pid]
# 分析工具
# - Eclipse MAT
# - VisualVM
# - JProfilerLinux工具
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文档,欢迎补充更多内存管理相关内容。