在学习一个技术之前,知道为什么要学习它,学习它有什么用是必要的,所以我要先说明为什么需要空间配置器:
内存碎片问题
假设依次向系统申请了16字节,8字节,16字节,4字节的空间,还剩下面的8字节空间未被分配;
随后,这两个16字节的空间被收回;
(淡紫色部分表示已经被系统收回,粉色部分表示已申请,白色部分表示未被分配)
这时一共有16+16+8(淡紫色+白色)= 40字节空间,我要申请一个24字节的空间;
看似没问题,但是却申请不了连续24字节的空间,这就是内存碎片的外碎片问题;
至于内碎片,下文会详谈。
内存碎片的危害:频繁申请小块内存,效率低下。
所以说空间配置器解决外碎片问题是必要的!
STL空间配置器分为一级和二级,我会逐一详细介绍,以下的代码是我自己通过剖析源码,造轮子简单实现的:
一:STL一级空间配置器
一级空间配置器在封装了malloc和free的基础上,模拟了C++的set_new_handler()机制来处理系统内存不足的问题;
具体情况我通过代码+分析的方式来呈现:
typedef void(*HANDLER_FUNC) ();
template <class inst>
class __Malloc_Alloc_Template
{
static HANDLER_FUNC __malloc_alloc_oom_handler;//static成员在类外初始化,默认初始化为NULL
public:
static void* Allocate(size_t n)
{
void* result = malloc(n);//申请n字节的空间,一级空间配置器先调用malloc
if (result == NULL)//如果申请失败,那么就会模拟C++set_new_handler()机制来处理
result = OOM_Malloc(n);
return result;
}
static void Deallocate(void* p/*, size_t n*/)
{
free(p);
}
//该函数是重新写一个处理问题的方法,并记录下旧方法
static void(*HANDLER_FUNC(void(*f)()))()
{
void(*old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return (old);
}
private:
static void* OOM_Malloc(size_t n)
{
while (1)
{
//__malloc_alloc_oom_handler默认初始化为空,处理方法为空时抛异常
if (__malloc_alloc_oom_handler == NULL)
throw bad_alloc();
//不为空的话,方法应该处理好问题,不然会一直循环
__malloc_alloc_oom_handler();
//假设新方法解决了问题,malloc申请n字节成功,则返回,否则一直循环直到解决问题!
void* ret = malloc(n);
if (ret)
return ret;
}
}
};
//初始化
template<class inst>
HANDLER_FUNC __Malloc_Alloc_Template<inst>::__malloc_alloc_oom_handler = NULL;
这就是一级空间配置器的原理,比较简单,接下来是测试:
此时没有重写处理方法,它默认为空会抛异常:
然后重写方法,就会根据重写的方法来解决问题,这里简单测试,方法里直接退出。
二:STL二级空间配置器
一级空间配置器只是开胃小菜,真正的大餐在二级空间配置器这里!
二级空间配置器的原理,简单来说:
1.当申请的内存大于128字节时,会直接调用一级空间配置器(因为申请的内存够大,不会产生外碎片);
2.引入了自由链表和内存池的机制:
自由链表的长度为16,每一个位置都挂有对应大小的内存块(比如第1,4,10个下分别挂有16字节,40字节,88字节的内存块),最大挂128字节的内存块(第15个);
当自由链表没有内存块时,则向内存池申请内存而不是直接向系统要,这样效率会高很多;
_startFree和_endFree指向内存池的开始和结束;
当内存池的空间不够时,才向系统申请,而且一次会申请很大块的内存,有效避免了频繁申请小块内存的问题。
当然,以上只是简单说明,具体很多细节,还是要通过代码体现:
二级空间配置器的成员数据:
private:
//内存池
static char* _startFree;//内存池开始的位置
static char* _endFree;//内存池结束的位置
static size_t _heapsize;//记录每次向系统申请内存池的大小的累加和。
//链接下一个
union Obj
{
Obj* _freeList_link;//指向下一个的指针
char client_data[1];
};
enum { _ALICN = 8 };//排列间隔
enum { _MAX_BYTES = 128 };//最大值,超过128调用一级空间配置器
enum { _NFREELISTS = _MAX_BYTES / _ALICN };//链的长度,为16
//自由链表,长度为16
static Obj* _freeList[_NFREELISTS];//指针数组
对于数据,我重点要说明两点:
1.union Obj
这是一个联合体,自由链表就是该联合体类型的,这样写的目的是,不管你挂多大的内存,我只需强转为Obj*类型,用一个_freeList_link指针来连接。
2.排列间隔为8:
排列间隔太大的话不好,会浪费空间,同样申请8字节空间,排列间隔如果为8,则刚好分配一个8字节的内存块;若排列间隔变大为16。那么最小只能分配一个16字节的空间,浪费了8字节的空间,这就是内碎片!
所以说空间配置器虽然解决了外碎片的问题,但还是存在内碎片的问题。
那么排列间隔如果设的很小呢,我可以这么说:8字节已经是最小了,因为在64位下,指针的大小是8字节,那么连Obj的一个指针都存不下了。
二级空间配置器初始化:
template <class inst, bool threads = true>
char* __Default_Alloc_Template<inst, threads>::_startFree = 0;
template <class inst, bool threads = true>
char* __Default_Alloc_Template<inst, threads>::_endFree = 0;
template <class inst, bool threads = true>
size_t __Default_Alloc_Template<inst, threads>::_heapsize = 0;
template <class inst, bool threads = true>
typename __Default_Alloc_Template<inst, threads>::Obj*
__Default_Alloc_Template<inst, threads>::_freeList[_NFREELISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
因为都是static成员,所以需要类外初始化。
二级空间配置器方法:
public:
static void* Allocate(size_t n)
{
cout << "二级空间适配器申请" << n << "字节空间" << endl;
//如果申请的大于128字节,用一级
if (n > _MAX_BYTES)
return __Malloc_Alloc_Template<int>::Allocate(n);
size_t index = FREELIST_INDEX(n);//在自由链表的位置
//当该位置有空间时,直接使用(头删),拿内存块返回
if (_freeList[index] != NULL)
{
cout << "自由链表第" << index << "处挂有内存,头删取一块" << ROUND_UP(n) << "大小的空间" << endl;
Obj* ret = _freeList[index];
_freeList[index] = ret->_freeList_link;
return ret;
}
else//该位置没有挂空间,那么就需要去内存池里找
{
cout << index << "处没有挂内存,需要从内存池里找" << endl;
//需要申请8的整数倍的空间 ROUND_UP(n)
return Refill(ROUND_UP(n));
}
}
static void Deallocate(void* p, size_t n)
{
//大于128.call一级释放
if (n > _MAX_BYTES)
{
__Malloc_Alloc_Template<int>::Deallocate(p);
return;
}
Obj* obj = (Obj*)p;//先把空间转为Obj*,再头插到自由链表的对应位置
size_t index = FREELIST_INDEX(n);
obj->_freeList_link = _freeList[index];
_freeList[index] = obj;
}
private:
static size_t FREELIST_INDEX(size_t n)
{
//当不是8的整数倍时,需要多给一些,比如10字节,就需要给16字节的空间
//那么会有6字节色空间没有用,这叫做叫内碎片
//if (n % 8 == 0)
// return ((n / 8) - 1);
//return n / 8;
//上面的办法虽然容易想到,但是下面的方法会更高效
return (((n + _ALICN - 1) / _ALICN) - 1);
}
static size_t ROUND_UP(size_t n)
{
//申请8的整数倍的空间
// (n+7)&(~7)
return ((n + _ALICN - 1) & (~(_ALICN - 1)));
}
static void* Refill(size_t bytes)
{
//比如需要开32字节的空间(butes为32):
//走到这里,说明自由链表挂32的位置为空,所以处理方法是区内存池里找
//具体机制:
//如果空间足够,就直接挂20个32字节的空间,以后再申请32字节的空间,就可以直接在自由链表拿内存块,提高效率
//如果空间不够,但至少得需要够1个32字节的空间
size_t nobjs = 20;//如果空间够,直接挂20块
char* chunk = ChunkAlloc(bytes, nobjs);
if (nobjs == 1)//如果只剩一个bytes的空间,直接返回该空间
{
cout << "系统只剩一块空间" << endl;
return chunk;
}
//如果是多个空间,需要返回一块,把剩下(nobjs-1)挂到相应的位置
//先挂起一块
size_t index = FREELIST_INDEX(bytes);
cout << "从系统中获取" << nobjs << "个" << bytes << "大小的内存块,返回一块,挂起19块到" << index << "处"<<endl;;
Obj* cur = (Obj*)(chunk + bytes);//chunk位置需要返回
_freeList[index] = cur;
for (size_t i = 1; i < nobjs - 1; i++)//只需要挂nobjs-2块
{
Obj* next = (Obj*)(char*)cur + bytes;
cur->_freeList_link = next;
cur = next;
}
//cur->_freeList_link = NULL;
return chunk;
}
static char* ChunkAlloc(size_t bytes, size_t& nobjs)
{
size_t leftBytes = _endFree - _startFree;//内存池剩余的内存
size_t totalBytes = bytes*nobjs;//一共挂20块需要多大的空间
cout << "内存池剩余" << leftBytes << "字节的空间,一共需要" << totalBytes << "字节的空间。" << endl;
if (leftBytes > totalBytes)//当内存池够用
{
char* chunk = _startFree;
_startFree += totalBytes; //_startFree往后移,内存池变小
return chunk;//所以这段需要的空间就是从chunk到_startFree这段
}
else if (leftBytes >= bytes)//不够20个bytes,但至少得够一个
{
nobjs = leftBytes / bytes;//得重新计算nobjs(剩下的内存够几个bytes)
totalBytes = nobjs*bytes;//同样,总共需要的空间也会变,也需要重新计算
char* chunk = _startFree;//挂了nobjs块,内存池往后移,返回需要的空间
_startFree += totalBytes;
return chunk;
}
else//当内存池已经连一个bytes的空间都没有了的机制
{
//1.虽然内存池没有bytes的空间,但是内存池还可能有一些空间
//比如,bytes为64,但只剩下32的空间了,所以一个bytes的空间也没有
//但是,还是需要处理这剩下的空间:在对应位置挂起剩余的空间
if (leftBytes > 0)//还有剩余的空间(小于bytes)
{
size_t index = FREELIST_INDEX(leftBytes);//剩余空间挂起来,对应自由链表的位置
cout << "内存池的空间大于0小于" << bytes << "字节,将剩余的" \
<< totalBytes << "字节挂到" << index << "位置" << endl;
((Obj*)_startFree)->_freeList_link = _freeList[index];
_freeList[index] = (Obj*)_startFree;
}
//2.找系统申请空间
size_t bytesToGet = totalBytes * 2 + (ROUND_UP(_heapsize >> 4));//需要申请的大小,一次申请比较大的一块内存
_startFree = (char*)malloc(bytesToGet);
if (_startFree == NULL)//如果连系统都没有那么多空间了,还是不能放弃
{
cout << "向系统申请" << bytesToGet << "字节空间失败" << endl;
//还是不放弃,到更大的自由链表中找:具体过程再总结一遍
/*比如说要申请32字节空间,32字节对应的自由链表没有挂内存,去内存池找,内存池
也没有这么多空间,再malloc向系统申请,系统也没有32字节的空间了,这时候可以
在自由链表更大的位置看看有没有挂内存块,比如如果64字节位置的自由链表挂有内
那么可以用。*/
for (size_t i = FREELIST_INDEX(bytes); i < _NFREELISTS; i++)//从bytes对应的位置找,直到自由链表的最后位置
{
if (_freeList[i])//如果更大的位置挂有内存块
{
_startFree = (char*)_freeList[i];
_freeList[i] = ((Obj*)_startFree)->_freeList_link;
_endFree = _startFree + (i + 1)*_ALICN;
//此时,把对应内存块拿给内存池
return ChunkAlloc(bytes, nobjs);
}
}
//没办法了,系统没空间了,更大的自由链表没有挂内存,调一级,其实到这里已经可以放弃治疗了
_endFree = 0;
_startFree = (char*)__Malloc_Alloc_Template<int>::Allocate(bytesToGet);
}
cout << "向系统申请" << bytesToGet << "字节成功,再次调用ChunkAlloc()" << endl;
_heapsize += bytesToGet;
_endFree = _startFree + bytesToGet;
return ChunkAlloc(bytes, nobjs);
}
}