底层机制
分割与合并
空闲列表(在堆上管理空闲空间的数据结构,不一定真是列表)包含一组元素,记录堆中的空闲空间,例如:
此时空闲列表会有两个元素,一个记录0~9字节,另一个记录20~29字节。
若申请一个字节的内存,分配程序会执行分割操作:找到一块可满足请求的空间,将其分割,第一块给用户,另一块留在空闲列表中。
若申请超过10字节的空间会咋样?
分配程序会在释放内存时合并可用空间,注意是相邻的。
例如:
合并后:
追踪已分配空间的大小
注意,调用free()时没有传数字,那么内存分配库是如何知道释放多少的空间呢?
大多数分配程序都会在头块(head)中保存额外信息。
例如:
ptr=malloc(20);
头块中的内容如下:
包含size记录分配空间大小和一个幻数(一个具体的数,但我们不知道其含义)。
当调用free()时,库会通过运算得到hptr的位置:
这个例子中,所以当我们调用free()时,实际上回收了20+sizeof(header_t)的大小。
嵌入空闲列表
如何创建一个这样的空闲列表?
它需要在空闲空间内创建。
假设我们现在有一个2^11(4096)字节的块。为了将它作为一个空闲列表来处理,我们需要初始化:
申请3个100字节的请求:
归还sptr所指的空间,改变head指针指的地址:
若剩余两个100内存也被释放:
释放顺序(以next为编号):
16708->16492->16384。
最后合并。
基本策略
保证快速和碎片最小化。
(碎片分为外部碎片和内部碎片,外部碎片是由于空闲空间被分割成不同大小的块,导致没有一块足够大的空闲空间,即使总的空闲空间足够大也不能满足请求;内部碎片是由于分配给出的内存块超出请求的大小产生的)
最优匹配(best fit)
遍历整个空闲列表,找到和请求大小一样或大于的块,返回其中最小的一块。
优点:避免空间浪费。缺点:性能差
最差匹配(worst fit)
跟最优匹配唯一区别是返回最大的一块,分割后将剩余块加入空闲列表。
缺点:开销大,产生过量碎片。
首次匹配(first fit)
找到第一个大于等于请求大小的空闲块,剩余空闲空间留给后续请求。
优点:速度快。缺点:让空闲列表开头有很多小块,解决办法:保持空闲块按内存地址有序,使合并操作容易,减少碎片。
下次匹配
不同于首次匹配每次从开头查找,它多维护一个指针指向上一次结束位置,避免对开头的频繁切割。
分离空闲列表
若某个应用程序经常申请一种(或几种)大小的空间,那就用一个独立的列表,只管理这样几个大小的对象。
优点:不会产生碎片,没有查找过程,内存分配和释放都很快。
伙伴系统
二分伙伴分配程序:
首先内存从概念上被看成2^N大小的空间,当有内存分配请求时,空闲空间被递归地一分为二,直到刚好可以满足分配大小(再分配就无法满足)。
由于此分配政策只允许2的整数次幂大小的空闲块,所以有内部碎片的风险。
优点:合并时,检查自己的伙伴(互为伙伴的块的地址只有一位不同)是否空闲,若是,就合二为一。继续向上检查,一直递归合并直到合并整个内存区域。