动态内存分配器管理heap(堆:虚拟内存区域的一部分)。
- 显式分配器:
应用分配并且回收空间(C 语言中的 malloc 和 free) - 隐式分配器(垃圾收集器):
检测已分配块不再被程序使用时就释放它。
限制:
- 不能操作和修改未分配的内存
- 不同的块需要对齐
1.碎片:
- 内部碎片
由于实际载荷的大小小于已分配块(例如对齐),所出现的无法利用的空间。 - 外部碎片
内存中没有足够的连续空间
2.如何分配:
- 空闲块的记录
- 分配
- 处理每次分配后空闲块的剩余部分
- 释放
隐式空闲链表
组成:
- 头部(指针)
- 块的大小
- 标记
- 有效载荷
- 填充(对齐或者碎片)
- 脚部:头部的副本(已分配的块不需要脚部)
搜索:
- 首次适配:从头开始搜索适合的空闲块
- 下一次适配:从上一次查询的地方开始查询
合并:
通过检查前/后面块的头部是否空闲合并邻近的内存块
- 立即合并:可能会导致抖动(合并后又马上需要分割)
- 推迟合并
获取新的堆内存:合并后依然不能获得合适的块,分配器通过sbrk函数向内核请求。
显式空闲链表(双向链表)
增加了头指针和尾指针
管理:
- LIFO(后进先出):将新释放的块放在链表的开始处
- 地址顺序
分离存储:
将堆分为多个链表数组,每个数组的块大小各不相同。
- 简单分离存储:
已分配的块如果有多余不会分割。 - 分离适配:
已分配的块如果有多余,分割剩余部分添加到适合的链表。 - 伙伴系统:
块的大小为2^n
3.垃圾收集
垃圾收集器将内存视为有向可达图,不可达点为垃圾。
当malloc无法调用合适的空闲块时,调用垃圾收集器通过free函数。
算法:Mark & sweep collection
- 标记出根节点所有可达和已分配的后继
- 释放未标记的已分配节点
可达节点组成二叉树。首先mark函数标记二叉树的节点,sweep函数将已标记的直接改成未标记,未标记的(不可达节点)直接释放。
内存错误:
- 解引用错误指针
例如在应该输入变量地址(&a)时变成了输入变量名(a)。一旦变量名被解析成合法的地址后覆盖原有内容,会难以发现原因。 - 读取未初始化的内存
例如使用未初始化的数组元素。 - 覆盖内存
例如栈缓存区溢出,off-by-one(错位:修改超出数组范围的位置) - 错误使用指针运算
在本应该移动指针的时候变成了移动指针指向的值。 - 内存泄漏(没有释放)
- 引用改变后的地址存储的数据。
例如在函数结束后使用局部变量的地址,或者引用释放后的数据。