什么是空间配置器?
空间配置器,就是用来为各个容器高效的管理空间(空间的申请与回收)的
为什么需要空间配置器
在模拟实现vector、list、map、unordered_map等容器时,所有需要空间的地方都是通过new申请
的,虽然代码可以正常运行,但是有以下不足之处:
- 空间申请与释放需要用户自己管理,容易造成内存泄漏
- 频繁向系统申请小块内存,容易造成内存碎片
- 频繁向系统申请小块内存,影响程序运行效率
- 直接使用malloc与new进行申请,每块空间前有额外空间浪费
- 申请空间失败怎么应对
- 代码结构比较混乱,代码复用率不高
- 未考虑线程安全问题
所以需要一个高效的内存管理机制来管理空间的申请与回收。
以128作为小块内存与大块内存的分界线,将空间配置器其分为两级结构,一级空间配置器处理大块内存(大于128byte),二级空间配置器处理小块内存。(小于128byte)
一级空间配置器
一级空间配置器的原理非常简单,就是底层封装malloc和free,并且增加了set_new_handler的思想
set_new_handler函数的作用是设置new_p指向的函数为new操作或new[]操作失败时调用的处理函数。
set_new_handler思想在这里的作用就是malloc空间申请失败后的处理机制;
如果内存申请失败:
- 如果程序员没有配置处理函数,配置器默认会抛异常;
- 如果程序员配置了处理函数,配置器则会按照该处理函数进行处理。
一级空间配置器的底层代码:
template <int inst>
class __malloc_alloc_template
{
private:
static void *oom_malloc(size_t);
public:
// 对malloc的封装
static void * allocate(size_t n)
{
// 申请空间成功,直接返回,失败交由oom_malloc处理
void *result = malloc(n);
if (0 == result)
result = oom_malloc(n);
return result;
}
// 对free的封装
static void deallocate(void *p, size_t /* n */)
{ free(p);}
// 模拟set_new_handle
// 该函数的参数为函数指针,返回值类型也为函数指针
// void (* set_malloc_handler( void (*f)() ) )()
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
};
// malloc申请空间失败时代用该函数
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;)
{
// 检测用户是否设置空间不足应对措施,如果没有设置,抛异常,模式new的方式
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);
}
}
typedef __malloc_alloc_template<0> malloc_alloc;
二级空间配置器
二级空间配置器专门负责处理小于128字节的小块内存,采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度与高效管理。
内存池
先申请一块比较大的内存块作为内存池,当需要内存时,直接到内存池中去取,当池中空间不够时,再向内存中去取,当用户不用时,直接还回内存池即可。避免了频繁向系统申请小块内存所造成的效率低、内存碎片以及额外浪费的问题。
工作原理
先申请好一个内存池作为备用,如果需要分配,就从内存池中分割,挂到哈希桶中,使用完毕后进行释放,将内存重新挂到对应位置的哈希桶中
内存块大小如何分配:
因为用户申请的空间基本都是4的整数倍,其他大小的空间几乎很少用到。因此:SGI-STL将用户申请的内存块向上对齐到了8的整数倍.
申请空间:
假如现在要申请 X byte的内存:先看哈希桶中有没有合适大小的内存对象,如果有就直接取走,如果没有,就去内存池中切一块X*20大小的内存,取走一个,剩下的19个放在对应的哈希桶中,下一次再申请X byte大小的内存,可以直接在这个哈希桶中获取。
如果这里申请的内存大小不是8的整数倍,向8的整数倍对齐就可以了。
释放空间:
释放X byte的内存对象,找到X对应的哈希位置,将内存对象直接挂到对应位置下面就可以
内存碎片问题
外碎片:
现在有大量内存被借出后还了回来,并且这些内存块是不连续的,不能满足我们新的大块内存申请需求,这就是外碎片。
内碎片:
假如向内存池申请 1 byte的内存空间,由于二级空间配置器的底层原理,可以分配的最小内存块是8 byte大小,那么剩余的7 byte就会被浪费掉,这个就叫内碎片。
为了解决上述的内存碎片问题,引入了伙伴系统以及slab分配器。