在之前我们已经简单介绍过STL中的一级空间配置器,并且进行了模拟实现
但是来看下面这个问题:
如图,阴影部分是我们开辟的不连续的小的内存块,空白区域是空闲的,如果现在要申请一段三个块大小的连续空间,可以申请出来吗?显然不能。但我们会发现系统剩余的内存明明足够我们需要的空间大小,可开辟太多了不连续的小块空间,导致没有办法开辟出来大块的内存,这就是外内存碎片的问题。
STL中的二级空间配置器就可以解决这个问题!!
二级空间配置器
来看看STL中二级空间配置器是怎么做的
如果要申请的空间超过128字节时,就用一级空间配置器处理,只有当申请的空间小于128字节时才调用二级空间配置器。
上一张图先来看看二级空间配置器的大致结构
二级空间配置器是以内存池管理的。每次从系统中配置一大块内存,并维护与之对应的自由链表free-list,下次如果再有相同大小的需要就直接从free-list中分配,如果用户归还内存时,将根据归还内存块的大小,将需要归还的内存插入到对应free-list的最顶端。
在图中我们可以看到,一共有16个free-list,从第一个开始每一个链表管理的区块分别是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节的小额区块,那设想如果用户申请的字节数不是8的倍数呢?我们的二级空间配置器会将申请的字节数上调至离该内存大小最近的8的倍数处。
可是这样我们会发现,真实分配给用户的空间,用户不一定能完全使用这些空间,还是浪费空间了啊?这就是内碎片问题,和我们刚开始说的外碎片问题相比,这个问题是不能避免的,可能就有人会说,那什么内存块就要以8字节为单位而不以1字节为单位呢?我们的内存块是像链表一样连接起来的,这样就必然需要指针来维护, 32位平台下指针4字节,64位平台下指针8字节,所以内存块最小也要能存放一个指吧,这样就清楚了为什么是8字节。
那么这个free-list到底是什么样子呢?来看看源代码就知道了:
union obj {
union obj* free_list_link;
char client_data[1]; /* The client sees this.*/
};
可以看到,与普通链表不同,obj用的是union,有没有觉得很巧妙呢?从第一个字段看,obj可以被看成是一个指向相同类型的另一个obj的指针,从第二个字段看它又可以被看成是一个指向实际区块的指针。这样就不会因为维护链表所必须的指针而造成另一种浪费。
好了,大致了解了一下二级空间配置器,我们来看看它的具体框架:
template <bool threads, int inst>
class __default_alloc_template
{
private:
#if ! (defined(__SUNPRO_CC) || defined(__GNUC__))
enum {_ALIGN = 8};//小型区块的上调边界
enum {_MAX_BYTES = 128};//小型区块的上限
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN,free-list的个数
# endif
/*将__bytes上调至8的倍数*/
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + _ALIGN-1) & ~(_ALIGN - 1));
}
__PRIVATE:
//free-list的节点构造
union obj {
union obj* free_list_link;
char client_data[1]; /* The client sees this.*/
};
private:
# ifdef __SUNPRO_CC
static obj* volatile free_list[];
# else
//16个free-list
static obj* volatile free_list[__NFREELISTS];
# endif
//根据区块大小,决定使用哪个free-list
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + _ALIGN-1)/_ALIGN - 1);
}
// Returns an object of size n, and optionally adds to size __n free list.
static void* refill(size_t n);
// Allocates a chunk for nobjs of size size. nobjs may be reduced
// if it is inconvenient to allocate the requested number.
static char* chunk_alloc(size_t size, int& nobjs);
// Chunk allocation state.
static char* start_free;//内存池起始位置
static char* end_free;//内存池结束位置
static size_t heap_size;
public:
/* n must be > 0 */
static void* allocate(size_t n)//申请空间
{...}
/* p may not be 0 */
static void deallocate(void* p, size_t n)//释放空间
{...}
//重新配置空间
static void* reallocate(void* p, size_t old_sz, size_t new_sz);
} ;
template <bool threads, int inst>
char* __default_alloc_template<threads, __inst>::_start_free = 0;
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::_end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::_heap_size = 0;
template <bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst> ::free_list[_NFREELISTS] =
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
看完二级空间配置器的大致框架,我们再来剖析里面的函数,就从空间配置函数allocate开始吧:
static void* allocate(size_t n)
{
obj * volatile * my_free_list
//如果申请空间大于128字节就调用一级空间配置器
if (n > (size_t) _MAX_BYTES) {
return (malloc_alloc::allocate(n));
}
//利用FREELIST_INDEX函数寻找16个free-list中合适的一个
my_free_list = free_list + FREELIST_INDEX(n);
obj* __RESTRICT __result = *my_free_list;
//没有可用的free-list,准备调用refill函数进行填充
if (__result == 0){
void *r = refill(ROUND_UP(n));
return r;
}
//找到了可用的free-list,将头部的内存块返回给用户
*my_free_list = __result -> free_list_link;
return __result;
};
假设我们要申请32字节的空间,根据FREELIST_INDEX函数找到对应下标3,即应该在free_list[3]中取区块,而且这时free_list[3]中有内存,我们就将第一个内存块返回给用户,并调整指针指向后面的内存块。
如果我们要申请得到内存大小为72字节呢?显然此时free_list中没有对应的内存块,这时候就需要调用refill函数填充free_list了
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
//直接向内存申请20块相同大小的内存块,然后我们定一个nobjs
//将它传给chunk_alloc函数。具体申请空间的过程由chunk_alloc函数去做
char* chunk = chunk_alloc(n, nobjs);(参数nobjs为引用)
obj* _VOLATILE* my_free_list;
obj* result;
obj* current_obj;
obj* next_obj;
int i;
//如果只获得了一个区块,就将这个区块直接返回给用户,此时free_list中无新节点
if (1 == nobjs) return(chunk);
//否则就说明得到不止一个区块,寻找合适的位置准备将新节点存入free_list中
my_free_list = free_list + FREELIST_INDEX(n);
//这里的my_free_list和next_obj 都是直接指向chunk+n的位置的
//因为第一个内存块需要返回给用户使用
result = (obj*)chunk;
*my_free_list = next_obj = (obj*)(chunk + n);
for (i = 1; ; i++) {//循环从1开始,因为第一块已经返回给用户了
current_obj = next_obj;
next_obj = (obj*)((char*)next_obj + n);
if (nobjs - 1 == i) {
current_obj -> free_list_link = 0;
break;
}
else {
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
如果内存池中也没有空间了呢?这时候就需要配置heap空间来补充内存池了
来看看补充内存池的函数chunk_alloc:
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
char* result;
//要分配的空间即20*size
size_t total_bytes = size * nobjs;
//计算内存池剩余空间
size_t bytes_left = end_free - start_free;
//剩余空间满足需求量
if (bytes_left >= total_bytes) {
//将空间返回给调用者
result = start_free;
start_free += total_bytes;
return(result);
}
else if (bytes_left >= size) {
//剩余空间不满足需求量,但至少可以提供一个size大小的空间
//同样将剩下的这些空间返回给调用者
nobjs = (int)(bytes_left/size);
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return(result);
}
else {
//内存池一个区块大小都不能开辟了
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
//如果内存池中还有残留的空间,就先将这些空间放入free_list中
if (bytes_left > 0) {
obj* _VOLATILE* my_free_list =free_list + FREELIST_INDEX(bytes_left);
((obj*)start_free) -> free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
//内存池已经没有一点空间了,开始配置heap空间来补充内存池
start_free = (char*)malloc(bytes_to_get);//调用malloc分配
if (0 == start_free) {
//走到这里说明malloc分配失败,heap空间不足
size_t i;
obj* _VOLATILE* my_free_list;
obj* p;
//去free_list中找找有没有没有用而且还比较大的区块
for (i = __size; i <= _MAX_BYTES; i += _ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (0 != p) {
//free_list中有尚未使用的区块,调整free_list释放出未用区块
*my_free_list = p -> free_list_link;
start_free = (char*)p;
end_free = start_free + i;
//递归调用字节,来调整nobjs
return(chunk_alloc(size, nobjs));
}
}
//哪都没有内存了,这时候调用一级空间配置器
//看其中的内存不足处理函数能不能起到作用,没有用就抛出异常
end_free = 0;
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return(chunk_alloc(size, nobjs));
}
}
觉得上面的注释应该可以帮助大家理解allocate函数了,对reallocate函数就不多做解释了,再来看看释放空间的函数deallocate:
static void deallocate(void* p, size_t n)
{
//释放内存大于128,调用一级空间配置器
if (n > _MAX_BYTES)
malloc_alloc::deallocate(p, n);
else {
//找到对应的free_list
obj* _VOLATILE* my_free_list = free_list + FREELIST_INDEX(n);
obj* q = (obj*)p;
// acquire lock(处理多线程的机制,在本文中不作解释)
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
//将要释放的空间从新头插进free_list中
q -> free_list_link = *my_free_list;
*my_free_list = q;
}
}
同样,在最后上一张图,来帮助大家理解二级空间配置器的工作原理: