一、STL为什么需要空间配置器?(内存有关)
1、解决内存碎片(外碎片)
外碎片:小块内存频繁分配导致小块空间不连续,分配不出大块的空间。
内碎片:比如需要3个字节,向上对齐取整new了8个字节,那么这5个未使用的为内碎片。
解决外碎片会引入内碎片
比如list、map、set、hashtable等push(new)或pop(delete)操作都有小空间。
2、提高效率:频繁分配空间,每一次都有系统调用
二、STL空间配置器框架:
一级空间配置器和二级空间配置器
1、一级空间空间配置器是对malloc、free和realloc的封装
(1)allocate调用malloc
(2)deallocate调用free
(3)模拟C++的set_new_hander()
set_new_hander():分配内存失败处理函数句柄。在内存空间分配失败的情况下,如果设置了该句柄,则不会直接抛异常,而是执行这个处理函数,因为__malloc_alloc_oom_handler是全局的,可在外释放一些空间,那么有可能下次循环就会分配成功。不断的尝试分配,直至分配成功;否则(未设置该句柄),抛出异常。
STL空间配置器使用的是malloc分配空间,所以设计的是set_malloc_hander()。
2、二级空间配置器
(1)维护16个自由串列(free_list)
(2)需求大于128,直接调用一级空间配置器
(3)需求不大于128,检查对应的free_list,如果free_list有可用的直接用,否则向上取整8、16、24…(允许小小的浪费)。然后调用refill(),为free_list重新填充空间。
设计:
free_list结构:
chunk_alloc():内存池给free_list提供内存块,由chunk_alloc()完成。
(1)当内存池剩余字节足够为需求分配,直接分配,并更新内存池水位线;
(2)如果内存池已经不够分配总需求,但是至少足够分给一个对象,那么能满足几个对象的大小就给几个;
(3)如果内存池已经不够一个对象的大小了,先把剩余的插入到自由链表合适的位置;
当内存池为空,申请两倍+的空间,一部分给free_list,一部分存在内存池。
如果大量申请导致整个系统的堆空间都不够了,malloc失败。那么chunk_alloc在free_list寻找可用空间块,若找到则交出,否则调用一级空间配置器,因为一级空间配置器也许有set_malloc_handler机制,有可能释放其他空间、分配成功,否则抛出异常。
char* chunkAlloc(size_t size,int& nobjs)
{
char* result ;
size_t bytesNeed=size*nobjs;//需求字节数
size_t bytesLeft=_endFree-_startFree;//内存池剩余字节数
if(bytesLeft>=bytesNeed)//剩余足够满足总需求
{
result=_startFree;
_startFree+=bytesNeed;//更新内存池水位线
return result;
}
else if(bytesLeft>=size)//剩余不够满足总需求,但可满足至少一个对象的大小
{
result =_startFree;
nobjs=bytesLeft/size;//先分配bytesLeft/size个对象
_startFree+=nobjs*size;//更新内存池水位线
}
else//内存池剩余不足一个对象的需求
{
if(bytesLeft>0)//剩余一些零头,先给free_list
{
size_t index=FREELIST_INDEX(bytesLeft);
((Obj*)_startFree)->_freeListLink=_freeList[index];
_freeList[index]=(Obj*)_startFree;
_startFree=NULL;
}
//从系统堆分配两倍+
size_t bytesToGet=2*bytesNeed+ROUND_UP(_heapSize>>4);
_startFree=(char*)malloc(bytesToGet);
//系统堆空间不足,分配失败
if(NULL==_startFree)
{
//先去自由链表找
for(int i=size;i<_MAX_BYTES;i+=_ALIGN)
{
Obj* head=_freeList[FREELIST_INDEX(size)];
if(head)
{
_startFree=(char*)head;
head=head->_freeListLink;
_endFree=_startFree+i;
return chunkAlloc(size,nobjs);
}
}
//自由链表无可用块,调一级空间配置器
_startFree=(char*)MallocAlloc::Allocate(bytesToGet);
}
}
}
static void* Allocate(size_t n)
{
//大于_MAX_BYTES_,调一级空间配置器的Allocate
if(n>(size_t)_MAX_BYTES_)
{
return(MallocAlloc::Allocate(n));
}
//否则在自由链表中找适当的
size_t index=FREELIST_INDEX(n);
Obj* result=_freeList[index];
if(NULL==result)
{
//自由链表无可用块,调refill()填充
return refill(ROUND_UP(n));
}
//调整自由链表
_freeList[index]=result->_freeListLink;
return (result);
}
void Dellocate(void* p,size_t n)
{
//若n大于128,直接还给一级配置器
if(n>(size_t)_MAX_BYTES_)
{
MallocAlloc::Dellocate(p,n);
}
//否则挂回到二级空间配置器自由链表的适当位置
else
{
size_t index=FREELIST_INDEX(n);
p->_freeListLink=_freeList[index];
_freeList[index]=p;
}
}
缺点:
1、引入内碎片,浪费空间
2、二级配置器分配的空间未还给操作系统,释放空间是挂在自由链表上(进程结束才释放)
事实上是它无法还给操作系统,因为内存池从系统堆得到的连续空间被切分成了块挂在自由链表上,而每一个块的使用和释放都不是统一的。假如链表的某一位置那一串想释放,但其中有个别还在使用。所以它还是释放不了。
3、如果一直分配,复用功能达不到
二级空间配置器本质上是想复用内存,自由链表从内存池得到一些内存,client用完后挂回去,下次有需要再从链表里拿。那如果一直分配而不释放,内存空间得不到复用。