空间配置器
1.为什么需要空间配置器
1.1内存碎片
从内存分配的角度来看,我们不免因为程序需求频繁申请、释放小块内存,从而在堆中造成外碎片,外碎片是指系统中空闲内存总量足够,但是不连续,所以无法分配给用户使用
注:内碎片是指已经分配给用户,用户却不利用的内存。如用户需要3字节,实际却得到了4字节,其中的1字节是浪费掉的
1.2频繁向系统申请小块空间,效率低
内存空间是由操作系统管理的,当我们要去开辟时,调用malloc( ),要进行用户态/内核态的切换,这样系统调用会产生性能问题,频繁地因为很小一块内存就进行系统调用,效率很低
2.空间配置器的实现
2.1整体策略
STL认为大于128字节的内存为大块内存,小于等于128字节的内存为小块内存,当请求的大块内存时,就使用第一层配置器分配内存,请求的小块内存则调用第二层配置器分配
2.2一级空间配置器
1)一级空间配置器的allocate()、deallocate()、reallocate()封装了malloc()、 free()、 realloc()等C库函数执行分配、释放、重新配置内存等操作
2)此外,为了处理内存不足的状况,一级空间配置器增加handle处理机制:在内存请求无法被满足时,调用设定的函数
3)一级空间配置器的allocate()如果调用malloc()分配内存不成功,将会调用内存不足处理函数oom_malloc(),oom_malloc()会循环调用错误处理函数__malloc_alloc_oom_handler,企图释放内存,再重新调用malloc(),直到分配成功;如果未设定错误处理函数,将直接bad_alloc异常信息
template <int inst> class __malloc_alloc_template { private: static void *oom_malloc(size_t); //使用malloc调用的内存不足处理函数 static void *oom_realloc(void *, size_t); //使用realloc调用的内存不足处理函数 static void(*__malloc_alloc_oom_handler)(); //错误处理函数 public: static void * allocate(size_t n) { void *result = malloc(n); //一级空间配置器直接调用malloc if (0 == result) result = oom_malloc(n); return result; } static void deallocate(void *p, size_t /* n */) { free(p); } static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz) { void * result = realloc(p, new_sz); //一级空间配置器直接调用realloc if (0 == result) result = oom_realloc(p, new_sz); return result; } static void(*set_malloc_handler(void(*f)()))() //设置错误处理函数 { void(*old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = f; return(old); } }; template <int inst> void * __malloc_alloc_template<inst>::oom_malloc(size_t n) { void(*my_malloc_handler)(); void *result; for (;;) { my_malloc_handler = __malloc_alloc_oom_handler; if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; //抛异常 } (*my_malloc_handler)(); result = malloc(n); if (result) return(result); } }
2.3二级空间配置器
1)二级空间配置器使用内存池+自由链表的机制:内存池负责向系统申请内存,分配小内存块给自由链表,自由链表共有16条,存储在数组free_list中,数组中由前往后的链表分别负责8字节,16字节,24字节,…,120字节,128字节的内存请求。为了便于管理,二级空间配置器在分配的时候字节数都是以8的倍数对齐,当内存请求字节数不是8的倍数时,将自动向上调整至8的倍数。当所需内存 n 小于等于128字节时,则去对应负责的自由链表上取内存块,当这块内存用完了,直接放回链表上即可回收;如果在申请内存时链表上没有内存块了,则向内存池申请内存块,此时会申请 nobjs 个(默认为20),将第1个返回给申请者,剩余的挂到对应链表上
2)为了节省内存,将不使用额外的指针串连自由链表上的节点,而是采用一物两用的方法:节点是一个union,由于union的特性,_freeListLink指针可以指向下一个节点,_clientData又可以是内存块的首地址,也就是使用内存块提供的便利来存放下一个节点的地址,把节点串起来形成链表
union _Obj //自由链表的节点 { _Obj* _freeListLink; //指向自由链表节点的指针 char _clientData[1]; //内存块首地址 };
3)内存池分配内存块给自由链表的过程(函数_chunkAlloc):
- 当内存池剩余的空间大小 leftBytes >= n x nobjs时,则直接分配好返回
- 当内存池剩余的空间大小 leftBytes 的范围是 [ n x 1, n x nobjs ),则这时候就分配 nobjs = leftBytes / n 这么多块的内存块返回
- 当内存池剩余的空间大小 leftBytes < n x 1 时,则先将剩余空间挂到自由链表上,再调用malloc()向系统申请 2 x n x nobjs + _GetRoundUp(_heapSize / 16) 字节的新内存。若申请成功,则再调用一次_chunkAlloc给自由链表分配内存块;若失败,则先去自由链表上找一块比 n 大的内存块,将它摘下来还给内存池,然后再调用一次_chunkAlloc分配内存,要是没找到,就调用一级空间配置器,看看内存不足处理机制能否处理
enum { _ALIGN = 8 }; //按照基准值8的倍数进行内存操作 enum { _MAXBYTES = 128 }; //自由链表中最大的块的大小是128 enum { _NFREELISTS = 16 }; //自由链表数组的长度,等于_MAXBYTES/_ALIGN template <bool threads, int inst> //非模板类型参数 class _DefaultAllocTemplate { union _Obj //自由链表的节点 { _Obj* _freeListLink; //指向自由链表节点的指针 char _clientData[1]; //内存块首地址 }; private: static char* _startFree; //内存池的头指针 static char* _endFree; //内存池的尾指针 static size_t _heapSize; //记录内存池已经向系统申请了多大的内存 static _Obj* volatile _freeList[_NFREELISTS]; //自由链表 private: static size_t _GetFreeListIndex(size_t bytes) //得到这个字节对应在自由链表中应取的位置 { return (bytes + (size_t)_ALIGN - 1) / (size_t)_ALIGN - 1; } static size_t _GetRoundUp(size_t bytes) //向上取成8的倍数 { return (bytes + (size_t)_ALIGN - 1)&(~(_ALIGN - 1)); //将n向上取成8的倍数 } static void* _Refill(size_t n); //在自由链表中申请内存,n表示要的内存的大小 static char* _chunkAlloc(size_t size, int& nobjs); //在内存池中申请内存nobjs块,每个对象size个大小 public: static void* Allocate(size_t n); //n要大于0 static void DeAllocate(void *p, size_t n); //n要不等于0 }; template<bool threads, int inst> char* _DefaultAllocTemplate<threads, inst>::_startFree = 0; //内存池的头指针 template<bool threads, int inst> char* _DefaultAllocTemplate<threads, inst>::_endFree = 0; //内存池的尾指针 template<bool threads, int inst> size_t _DefaultAllocTemplate<threads, inst>::_heapSize = 0; //记录内存池已经向系统申请了多大的内存 template<bool threads, int inst> typename _DefaultAllocTemplate<threads, inst>::_Obj* volatile //前面加typename表示后面是个类型 _DefaultAllocTemplate<threads, inst>::_freeList[_NFREELISTS] = { 0 }; //自由链表 //分配空间 template<bool threads, int inst> void* _DefaultAllocTemplate<threads, inst>::Allocate(size_t n) //n:要申请的字节数 { void *ret; //大于_MAXBYTES(128)个字节则认为是大块内存,直接调用一级空间配置器 if (n > _MAXBYTES) { ret = malloc_alloc::_Allocate(n); } else //否则就去自由链表中找 { _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n); //让myFreeList指向自由链表中n向上取8的整数倍 _Obj* result = *myFreeList; if (result == NULL) //如果这条链表上没有挂内存块,则就要去内存池中申请 { ret = _Refill(_GetRoundUp(n)); //到内存池中申请 } else //已经在自由链表上找到了内存块 { *myFreeList = result->_freeListLink; //把第2个内存块的地址放到自由链表上 ret = result; //第1个作为返回值 } } return ret; } //回收空间 template<bool threads, int inst> void _DefaultAllocTemplate<threads, inst>::DeAllocate(void *p, size_t n) //n:要申请的字节数 { //如果n大于128,就直接调用一级空间配置器的释放函数 if (n > _MAXBYTES) { malloc_alloc::_DeAllocate(p); } else //否则将这块内存回收到自由链表中 { _Obj* q = (_Obj*)p; _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n); q->_freeListLink = *myFreeList; *myFreeList = q; } } //自由链表向内存池取内存 template<bool threads, int inst> void* _DefaultAllocTemplate<threads, inst>::_Refill(size_t n) //n表示要申请的字节数 { int nobjs = 20; //默认向内存池一次性申请20块 char* chunk = _chunkAlloc(n, nobjs); //调用_chunkAlloc()向内存池申请,nobjs是引用传参,可以改变 if (1 == nobjs) //如果内存池只分配了1块,则直接返回给调用者 { return chunk; } //如果分配了多块,则返回第1块给调用者,其他挂在自由链表上 _Obj* ret = (_Obj*)chunk; //将第1块作为返回值 _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n); *myFreeList = (_Obj*)(chunk + n); //将第2块的地址放到自由链表上 _Obj* cur = *myFreeList; _Obj* next = NULL; for (int i = 1; i < nobjs; ++i) //将剩下的块挂到自由链表上 { next = (_Obj*)((char*)cur + n); cur->_freeListLink = next; cur = next; } cur->_freeListLink = NULL; return ret; } //内存池向系统申请内存 //size:小内存块的字节数,nobjs:要申请的块数 template<bool threads, int inst> char* _DefaultAllocTemplate<threads, inst>::_chunkAlloc(size_t size, int& nobjs) { char* result = NULL; size_t totalBytes = size * nobjs; //所请求的内存大小 size_t leftBytes = _endFree - _startFree; //内存池剩余的大小 if (leftBytes >= totalBytes) //内存池剩余大小足够分配nobjs块 { result = _startFree; _startFree += totalBytes; return result; } else if (leftBytes >= size) //内存池剩余大小不够分配nobjs块,但至少够1块 { nobjs = (int)(leftBytes / size); result = _startFree; _startFree += (nobjs*size); return result; } else //内存池剩余大小连1块都不够分配了 { if (leftBytes > 0) //把内存池的零头挂到自由链表上 { _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(leftBytes); ((_Obj*)_startFree)->_freeListLink = *myFreeList; *myFreeList = (_Obj*)_startFree; } //内存池调用malloc()开辟新内存,一次性开辟 size_t NewBytes = 2 * totalBytes + _GetRoundUp(_heapSize >> 4); _startFree = (char*)malloc(NewBytes); if (0 == _startFree) //开辟失败 { //开辟失败的话,首先去自由链表上找一块比n大的内存块 for (size_t i = size; i < (size_t)_MAXBYTES; i += (size_t)_ALIGN) { _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(i); _Obj* p = *myFreeList; if (NULL != p) //在自由链表找到一块内存块 { _startFree = (char*)p; //将这个内存块摘下来还给内存池 *myFreeList = p->_freeListLink; _endFree = _startFree + i; return _chunkAlloc(size, nobjs); //内存池开辟好的话,就再调一次chunk分配内存 } } //要是找不到的话,就调一级空间配置器,其中有内存不足处理机制 _endFree = NULL; _startFree = (char*)malloc_alloc::_Allocate(NewBytes); } //开辟成功,就更新heapSize,更新_endFree _heapSize += NewBytes; _endFree = _startFree + NewBytes; return _chunkAlloc(size, nobjs); //内存池开辟好的话,就再调一次chunk分配内存 } } typedef _DefaultAllocTemplate<0, 0> default_alloc;
3.空间配置器存在的问题
1)内碎片:容易产生内碎片,自由链表上所挂的内存块的大小都是8字节的整数倍,因此当我们需要非8倍数的内存块,往往会导致浪费,比如我只要1字节,但是自由链表最低分配8字节,也就浪费了7字节。这一点在计算机科学中很常见
2)没有释放自由链表上所挂内存块的函数:空间配置器中所有的函数和变量都是 static 的,那么他们是存放在数据段的,又没有写释放他们的函数,所以在程序结束的时候才会释放他们,这样就导致自由链表一直占用着内存
posted on 2019-03-22 08:34 JoeChenzzz 阅读(275) 评论(0) 编辑 收藏 举报