我們都知道STL裏有很多容器,如vector,stack,queue等等,每個容器都是通過配置器來獲取存儲空間的,現在來總結下配置器原理
當我們創建一個對象時常用方式是new,銷燬一個對象用delete
T* a = new T;
Delete a;
對於new 其實是先調用爲其配置內存,再調用相應的構造函數,對於delete是先調用析構函數再釋放內存
內存配置操作是有allocate來完成,構造由construct完成
主要來看空間的配置,配置器定義位於<memory>中,其中負責內存空間的配置和析構的主要在下面頭文件中
#include <stl_alloc.h>
- SGI兩層配置器
SGI設置了兩級配置器,對於大於128字節的申請用第一級配置器,小於則用第二級配置器,他們拿到內存空間的途徑不同,主要思想是大塊內存就直接malloc從內存裏申請,小塊的就先從內存池裏申請,這樣可以避免產生很多的外碎片。
- 一級配置器
直接調用malloc/free來申請空間,並有異常狀態處理機制,即當內存配置需求無法被滿足時會調用用戶自己定義的一個指定函數,來看源碼
template<int Inst>
class __MallocAllocTemplate //一級空間配置器
{
typedef void (*OOM_HANDLER)();
private:
static void* OOM_Malloc(size_t n);
static void* OOM_Realloc(void *p, size_t newSZ);
static OOM_HANDLER OOM_Handler;
public:
static void* Allocate(size_t n) //先調用allocate來申請內存空間
{
void* ret = malloc(n);
if (ret == NULL) //當沒有這麼大小的內存塊是調用OOM_Malloc
ret = OOM_Malloc(n);
return ret;
}
static void Deallocate(void* p, size_t n)
{
free(p);
}
static void* Reallocate(void* p, size_t oldSZ, size_t newSZ)
{
void* ret = realloc(p, newSZ);
if (ret == NULL)
ret = OOM_Realloc(p, newSZ);
return ret;
}
static OOM_HANDLER SetMallocHandler(OOM_HANDLER f)
{
OOM_HANDLER old = OOM_Handler;
OOM_Handler = f; //獲取用戶自己設定的處理函數
return old;
}
};
template<int Inst>
void (*__MallocAllocTemplate<Inst>::OOM_Handler)() = NULL;
template<int Inst>
void* __MallocAllocTemplate<Inst>::OOM_Malloc(size_t n)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL) //如果用戶沒有設定出錯處理函數,就拋出 bad_alloc異常信息
throw bad_alloc();
(*myHandler)();
ret = malloc(n);
if (ret != NULL)
return ret;
}
}
template<int Inst>
void* __MallocAllocTemplate<Inst>::OOM_Realloc(void* p, size_t newSZ)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL)
throw bad_alloc();
(*myHandler)();
ret = realloc(p, newSZ);
if (ret != NULL)
return ret;
}
}
typedef __MallocAllocTemplate<0> MallocAlloc;
可以看到allocate和realloc都是在調用malloc和realloc不成功後,改調用oom_malloc()和oom_realloc()
- 二級配置器
二級配置器管理了一個內存池並維護了一個自由鏈表,自由鏈表通過有16個節點。首先配置器會主動將任何小額區塊的內存需求量上調至8的倍數,然後檢查對應的free list,如果free-list中有可用的區塊,就直接拿來,如果沒有,就準備爲對應的free-list 重新填充空間
static void* Allocate(size_t n)
{
//...
if (ret == NULL)
{
void* r = Refill(RoundUP(n));
}
//...
}
新的空間將取自內存池,缺省取20個新節點,如果內存池不足(還足以一個以上的節點),就返回的相應的節點數
static void* Refill(size_t n)
{
size_t nobjs = 20;
char* chunk = (char*)ChunkAlloc(n, nobjs); //默認獲得20的新節點
if (nobjs == 1) //如果只有一塊直接返回調用者
return chunk;
//有多塊,返回一塊給調用者,其他掛在自由鏈表中
Obj* ret = (Obj*)chunk;
Obj* cur = (Obj*)(chunk + n);
Obj* next = cur;
Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
*myFreeList = cur;
for (size_t i = 1; i < nobjs; ++i)
{
next = (Obj*)((char*)cur + n);
cur->freeListLink = next;
cur = next;
}
cur->freeListLink = NULL;
return ret;
}
如果當內存池中連一個節點大小都不夠時,就申請新的內存池,如果申請內存池成功就把原來內存池中剩下的空間分配給適當的free-list,如果找不到就調用第一級配置器,總的概括策略如下
- 將n字節擴展到8的倍數,找自由鏈表,有則分配,沒有則向內存池申請20×n的內存塊
- 若內存池小於20x n,但是比一塊大小n要大,那麼此時將內存最大可分配的塊數給自由鏈表
- 如果整個內存池空間都不夠了,就先將內存池殘餘的零頭給掛在自由鏈表上,然後向系統heap申請空間,申請成功則返回,申請失敗則到自己的自由鏈表中看看還有沒有可用區塊返回,如果連自由鏈表都沒了最後會調用一級配置器
小結:可以看到STL內存分配器是基於空閒鏈表的最佳分配策略,對小內存申請進行了優化,可以避免內存碎片產生同時最大化內存利用率