浅谈内存碎片
内存碎片是如何形成的
内存碎片是操作系统中一个常见的内存管理问题,它会导致内存利用率下降,甚至使得明明有足够的总空闲内存,却无法分配出一块连续的大块内存给程序使用。根据形成方式的不同,内存碎片分为外部碎片和内部碎片两类。
一、外部碎片
外部碎片出现在动态分区分配的内存管理方式中,即系统按程序的实际需求动态地分配和释放内存块,分配时从空闲内存中切割出一块刚好满足(或略大于)需求大小的连续区域。
1. 形成过程
假设初始内存是一片连续的空闲区域(假设总大小为 30 个单位,地址 0~29):
-
时刻 1:进程 A 申请 8 个单位。系统从空闲区开头分配地址 0~7。
-
时刻 2:进程 B 申请 6 个单位。分配地址 8~13。
-
时刻 3:进程 C 申请 10 个单位。分配地址 14~23。
-
此时空闲区只有两段:地址 24~29(6 个单位),以及之前被释放的可能区域(但暂未释放任何进程)。
-
时刻 4:进程 B 运行结束,释放它占用的地址 8~13。此时空闲区变为:
- 空洞 1:地址 8~13(6 个单位)
- 空洞 2:地址 24~29(6 个单位)
-
时刻 5:进程 D 申请 8 个单位。虽然总空闲内存为 12 个单位,但没有一块连续的空闲区能容纳 8 个单位(两个空闲块分别只有 6 和 6)。因此分配失败,即使总空闲内存足够。
这就是外部碎片:许多小的、不连续的空闲块(空洞)散布在整个内存空间中,导致无法满足较大的连续内存分配请求。
2. 产生的根本原因
- 分配与释放的顺序不可预测:进程执行时间不同,释放内存的时间点随机,使得原有的大块空闲区被“切割”成许多小块。
- 分配策略的“按需切割”:每次分配时,系统从某个空闲块中精确切出所需大小,剩下的一小段继续作为空闲块。多次这样的操作会产生大量微小空闲块。
3. 典型例子(首次适应算法下的碎片积累)
假设初始空闲块:0~99(100 单位)。
操作序列:
- 分配 20(0~19),剩余空闲 [20~99](80 单位)
- 分配 30(20~49),剩余空闲 [50~99](50 单位)
- 释放 0~19,空闲变为 [0~19](20 单位)和 [50~99](50 单位)
- 分配 25(找第一个能容纳的块:0~19 不够,50~99 够),从 50~74 分配 25,剩余空闲 [75~99](25 单位)和 [0~19](20 单位)
- 释放 20~49,空闲变为 [0~19](20 单位)、[20~49](30 单位)、[75~99](25 单位)—— 虽然连续两个区间 [0~19] 和 [20~49] 相邻,但如果不做合并,它们仍是分开的空闲块。
实际上,好的内存分配器会合并相邻空闲块,但即使合并,在多次分配释放后,仍然可能产生许多与已分配块交替出现的小空闲块,无法形成足够大的连续区域。
二、内部碎片
内部碎片出现在固定分区分配或基于固定大小块的内存管理方案中,例如:
- 固定分区系统:将内存划分为多个固定大小的分区(如 8KB、16KB、32KB),每个分区一次只能装入一个进程。
- 分页系统:每个进程分配整数个页面(页是固定大小,如 4KB)。
- 专用对象分配器(如 Slab 分配器)中,每个缓存针对固定大小的对象。
1. 形成过程
当分配的内存块大小大于请求的实际大小时,块内未被使用的部分就是内部碎片。
例子(分页系统):
- 页大小 = 4KB(4096 字节)
- 进程申请 4100 字节的数据区。
- 操作系统需要分配 2 个页(8192 字节)才能满足 4100 字节的需求。
- 实际使用 4100 字节,浪费了 8192 − 4100 = 4092 字节。这个浪费就是内部碎片。
例子(固定分区):
- 系统有一个 32KB 的分区。
- 一个进程实际只需要 20KB,但必须装入这个 32KB 的分区。
- 分区内浪费的 12KB 就是内部碎片。
2. 产生的根本原因
- 分配粒度(granularity):内存管理以固定大小的块(页面、分区、缓存行)为最小单位,无法按任意字节进行分配。
- 对齐要求:有些硬件或系统要求内存地址按特定边界(如 8 字节、4KB)对齐,导致分配时实际占用的空间略大于申请的大小。
- 固定分区设计:为简化管理,将内存划分成若干固定大小区域,但进程大小与分区大小很难完美匹配。
三、内部碎片与外部碎片的对比
| 特性 | 外部碎片 | 内部碎片 |
|---|---|---|
| 产生位置 | 已分配块之间的空闲区域 | 已分配块的内部 |
| 管理方式 | 动态分区分配 | 固定块分配(分页、固定分区) |
| 是否可避免 | 可通过紧凑技术缓解,但无法根除 | 可减小分配粒度来降低,但无法完全消除 |
| 对系统的影响 | 大块连续分配失败 | 浪费内存,但不会导致分配失败 |
| 典型场景 | 长时间运行的多道程序系统 | 分页操作系统、专用缓存 |
四、如何减少或处理内存碎片
- 紧凑(Compaction):移动已分配的内存块,将所有空闲区合并成一个大块。需要支持动态重定位(如使用基址寄存器)。开销较大。
- 伙伴系统(Buddy System):以 2 的幂次大小分配和合并,可显著减少外部碎片,但会产生内部碎片。
- 分页机制:通过页表将逻辑上连续但物理上分散的页面映射起来,从而完全消除外部碎片(因为物理上不需要连续)。这是现代操作系统采用的主要方法。
- Slab 分配器:针对内核中频繁分配释放的固定大小对象(如 task_struct、inode 等),预先分配对象池,避免反复切分大块内存,从而减少外部碎片,同时内部碎片控制在对象大小范围内。
- 最佳适应或最坏适应算法:相比首次适应,能减缓外部碎片产生速度,但会增加分配查找时间。
总结
- 外部碎片:由于动态分配和释放的随机性,大块空闲区被不断切割成无法合并的小空闲块,导致无法满足大连续请求。
- 内部碎片:由于分配粒度固定(页、分区、对象缓存),申请大小不是粒度的整数倍时,块内剩余空间被浪费。
理解内存碎片的成因,有助于设计高效的内存管理策略,平衡内存利用率与分配速度。在现代通用操作系统中,分页机制配合按需分配基本消除了外部碎片,而内部碎片(页内浪费)则作为换取灵活性和简单性的代价被接受。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)