我们知道C++内存配置操作和释放操作是这样的:
class Foo {...};
Foo* pf = new Foo; //配置内存,然后构造对象
delete pf; //将对象析构,然后释放内存
new 包含两个阶段操作:1、调用operator new 配置内存。2、调用构造函数,构造对象内容。
delete也内含两个阶段操作:1、调用析构函数。2、调用operator delete 释放内存。
为了精密分工,STL 将这两个阶段操作区分开来。内存配置操作由成员函数 alloccate() 负责,内存释放由 deallcate() 负责;对象构造由 construct() 负责,对象析构则由 destroy() 负责。
在内存分配的过程中,会有几个问题需要考虑:
为了解决这些问题,SGI STL设计了 双层级配置器,也就是第一级配置器和第二级配置器。第一级配置器直接使用 malloc() 和 free() 。第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes 时,视之为 “足够大”,便调用第一级配置器;当配置区块小于 128 bytes 时,视之为 “过小” ,为了降低额外负担,便采用复杂的内存池管理方式。
SGI的第一级配置器以 malloc(), free(), realloc() 等C函数执行实际的内存配置、释放、重配置操作。当 malloc 或者 realloc 调用不成功后,改调用 oom_malloc() 和 oom_realloc() 。后两者都有内循环,不断调用内存不足处理例程,期望在某次调用之后,获得足够的内存。但如果内存不足处理例程未被客户端设定,则直接抛出 bad_alloc 异常,或者终止程序。
注意:设计内存不足处理例程是客户端的责任,设定内存不足处理例程也是客户端的责任。
二级配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。它是用一个16个元素的自由链表(free_list)来管理的,每个位置的内存大小都是8的倍数,分别为:8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128。
free_list的节点结构如下,使用union是为了节省内存,这样每个节点就不需要额外的指针:
union obj
{
union obj* free_list_link;
char client_data[1];
};
内存池与自由链表 free_list 之间的关系如下图所示:
其中free_list的第一个元素指向 8个字节的空间,8个字节的空间我给分配了10个。free_list的最后一个元素指向128个字节的空间,此空间我给分配了4个。
free_list管理的是内存池中已经分配给 free_list 且尚未使用的内存,如果系统想从free_list中拿一8字节内存,则直接从free_list[0]中弹出顶部第一个元素,然后顶部后移。
主要分为四种情况:
当用户从二级空间配置器中申请的内存被释放时,二级空间配置器将回收的内存插入到对应的 free_list 中,而不是直接释放这块内存。其流程如下:
一级配置器的实现思想实际就是对c下的malloc,relloc,free进行了封装,并且在其中加入了c++的异常。我们要了解对于当内存不足调用失败后,内存不足处理是用户需要解决的问题,STL不处理,只是在你没有内存不足处理方法或有但是调用失败后抛出异常。
二级配置器是通过内存池和空闲链表配合起来的一个特别精巧的思想。
空闲链表的每个节点分别维护以8的倍数的各内存大小(8,16,32…128字节)。首先,用户申请内存小于128个字节,进入二级配置器程序,假如第一次用户申请内存为32字节,程序直接申请40个大小的32字节空间,一个交给客户,另外19个交给空闲链表,剩下的20个交给内存池。接下来,第二次申请空间,假设这次用户需要64字节的空间,首先,程序检查64字节的空闲链表节点是否有空闲空间,如果有,直接分配,如果没有,就去内存池,然后检查内存池的大小能分配多少个64字节大小的节点,完全满足,则拿出一个给用户,剩下的19个给空闲链表,如果剩余的空间不够分配20个64字节大小的节点,尽可能分配内存池的最大个数给用户和空闲链表。如果一个都分配不出,首先把内存池中的剩下没用的小内存分给相应适合的空闲链表,然后继续调用S_chunk_alloc申请内存,内存申请不足,去检索空闲链表有没有尚未使用的大的内存块,如果有,就拿出来给给内存池,递归调用S_chunk_alloc,如果空闲链表也没有了,则交给一级适配器(因为其有对内存不足处理的方法)。内存够,则拿到申请的内存补充内存池。重复上面的操作,将一个给客户,其他的19个交给空闲链表,剩下的给内存池。