深入了解计算机系统(堆分配和回收策略)
堆内存分配器在吞吐量(单位时间内处理请求的次数)和利用率之间把握权衡,必须考虑一下几个问题:
1:空闲块的组织:我们如何记录空闲块;
2:放置: 我们如何选择一个合适的空闲块来放置一个新分配的块?
3:分割 :载我们将一个新分配的块放置到某个空闲块之后,我们如何处理这个空闲块剩余的部分?
4:合并: 我们如何处理一个刚刚释放的块?
a:隐式空闲链表:
分配器都需要一些数据结构,允许它来区别块边界,以及区别已分配块和空闲块,大多数分配器讲这些信息潜入在块的本身;
在这种情况下,一个快是有一个固定大小的头部(一个字),有效载荷,邮寄可能的一些额外的填充组成的,投鞭编码了这个块的大小(包括头部和所有填充,)以及这个块是已分配的还是空闲的,如果强加一个最小大小限制(二个字),那么块大小就是最小大小限制的倍数(8字节的倍数),用头部(1个字,假定,32bit)的29个高位来存储块的大小,低三位来编码其他信息,低三位中的最低位(标志位)来表示该块分配否(0,1);
有效载荷后面是一片不使用的填充区域,需要填充的原因很多,e.g.应对外部碎片,或者满足对齐方式;
这种结构称之为隐式空闲链表,是因为空闲块 是通过头部中的大小字段隐含的连接在一起的;
优点:简单,缺点:空闲链表的搜索与堆中的以分配的块和空闲块总数显线性关系;
意识点一点就是系统对齐方式要求和分配器对块格式的选择会对分配器上的最小块大小有强制要求;
b:放置以分配的块:
放置策略:
1:首次适配:从头开始搜索空间链表,选择第一个合适的空闲块。
2:下一次适配:和首次适配很相识,只不过不是链表开始出开始每次搜索,而是从上一次查询结束的地方开始。
3:最佳适配:搜索每一个空闲块,选择合适所需请求大小的最小空闲块。
首次优点:将大的空间块保留在列表的后面,缺点:靠近列表起始处留下空闲块的碎片,增加了对较大块的搜索时间。
下次:knuth提出,比首次运行快一些,尤其是列表前面布满许多小的碎片时,然而下次比首次利用率要低得多。
最佳:利用率最高,但由于对堆的彻底的搜索,时间复杂度高;
c:分割空闲块:
一旦分割器找到一个匹配的空闲块,他就必须做另一个决定,那就是分配这个 空闲块中多少空间,一个是选择整个空间,简单快捷,但缺点是内部碎片,如果放置策略趋于产生好的匹配,额外的内部碎片可以接受;
然而,如果匹配的不太好,那么分配器通常会选择讲这个空间块分割为两个部分,一个部分为分配块,另外变成一个空的空闲块。如果分配器不能为请求找到合适的空闲块将发生什么?
1.一个是选择通过合并那些在存储器上物理相邻的空闲块来创建一个更大的空闲块,2:然而这样还是不能得到足够大的块,分配器就会想内核请求额外的堆空间。
d:合并空闲块:
当分配器释放一个已分配块时,可能有其他空闲块与这个释放块相邻。这些邻接的空闲块肯恩那个引起一种现象,"假碎片",就是许多可用的空闲你看被切割成小得,无法使用的空闲块,比如两个相邻的4Bytes的空闲块,这时请求大小为5bytes的空闲块,只有两者合并才能满足,单独一个都满足不了该请求,
为了解决假碎片的问题,任何实际的分配器都必须合并相邻的空闲块,这个过程叫做合并,这就出现一个重要策略决定,也就是在每次一个块释放时,就合并所有的空闲块?还是推迟合并,知道某个请求分配失败,然后再去扫描整个堆,合并所有空闲块。
e:带边界标志的合并:
分配器如何实现合并的? 让我们称想要释放的块为当前块,那么,合并下一个空闲块很简单搞笑,当前块的头部指向下一个块的头部,可以检查这个指针以旁当下一个块是否为空闲块,如果是,就将他的大小简单的加到当前块的头部的大小上,常数时间解决;
但是我们该如何合并前面的块呢?给定头部的隐式空闲列表,意味的选择是将搜索整个列表,记住当前块的为之,只当我们到达当前块,(线性时间);
Knuth(又是他,算法的鼻祖,神人)提出了一种技术,叫做边界标记,允许常数时间内对前面的块进行合并,在每个块的结尾处添加一个头部的一个副本(相对于当前块开始地址的偏移量固定),然后查看上个块的尾部(头部副本)的最低bit来判断是否为空闲块。确定空间浪费更严重。