1. 开场白
在一开始说string的时候,就说到了STL。但是其中之一的空间配置器到现在才记录,可能有些晚。因为空间配置器可以说是STL的根基。《STL源码剖析》的作者,侯捷老师第一个介绍的就是空间配置器。
2. 空间配置器
在前面模拟实现STL容器时都是new出来的,但是会有如下问题:
- 空间申请与释放需要用户自己管理,容易造成内存泄漏
- 频繁向系统申请小块内存块,容易造成内存碎片,影响程序运行效率
什么是小块内存?SGI-STL以128作为小块内存与大块内存的分界线,于是空间配置器以128字节分为两级结构,一级空间配置器处理128字节以上大块内存,二级空间配置器处理不超过128字节的小块内存。
3. 一级空间配置器
一级空间配置器简单的封装了malloc、free,realloc来实现内存的申请、释放、重配置,然后增加了C++的内存分配失败后处理机制,就是C++的operator new,只是类似,并不是就是operator new。
如果一级空间配置器申请空间失败后,会调用oom_malloc或者oom_realloc,这两者在内部实现的时候是这样:死循环调用内存申请失败处理函数
,直到申请到空间,如果我们没有设置这个内存申请失败处理函数
,那么失败后直接抛出异常或者直接终止程序。
看一下源码吧。
// malloc_alloc out-of-memory handling
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int inst>
//初始化是 0,且是条件编译
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif
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);
}
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, 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 = realloc(p, n);
if (result) return(result);
}
}
4. 二级空间配置器
下面主要要说的是二级空间配置器,不仅因为二级空间配置器使用频繁,而且机制较为复杂。
二级空间配置器专门负责处理小于128字节的小块内存。为了提升小块内存的申请与释放的方式呢,SGI-STL采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度与高效管理。
4.1 内存池
先申请一块比较大的内存块已做备用,当需要内存时,直接到内存池中去去,当池中空间不够时,再向内存中去取,当用户不用时,直接还回内存池即可。避免了频繁向系统申请小块内存所造成的效率低、内存碎片以及额外浪费的问题。
下面说一下内存池是怎么管理内存的。首先要解决下面几个问题。
1、当用户需要空间时,是否直接从内存池中大块空间中直接截取?
答案是否定的。
2、 对用户归还的空间能否直接拼接在大块内存前?
答案是否的。因为归还空间的顺序不一定就是申请的顺序,这样的空间是不连续的,是不能直接拼接的,如:
3、对用户归还的空间如何进行管理?
答案是:链表管理。对于归还的空间以链表的形式进行管理,归还一块内存,在链表上尾插。
4、 不断切割会有什么后果?
答案是造成内存碎片,因为每次拿取的时候,会从大内存上进行切割,不一定该内存就正好和用户申请的一样大,如:现在链表上有8字节的内存,而用户要7字节的空间,现在剩下1字节的碎片无法使用。
4.2 哈希桶设计
由于用户申请空间时在查找合适的小块内存时效率比较低,并且会造成内存碎片,因而SGI-STL没有采用链表的方式对用户已经归还的空间进行管理,而是采用了哈希桶的方式进行管理。 哈希表挂了16个桶。每一串桶的大小以8字节大小开始,以8字节的倍数增长,一共分了16块,最大的一串就是128字节。如下图
为什么是8的整数倍?
因为申请空间时都是自定义类型,对于C++来说,没有虚白指针的空类是一个字节,而有一个成员的至少也是4个字节,对于编译器不同,默认对齐数,但结构体或者类的大小都是4或者8的倍数,因此以8的倍数增长。
对了,是否对该节点有疑问?比如增加了一个指向下一个结点的指针,增加了负担?
union obj {
union obj * free_list_link;
char client_data[1]; /* The client sees this. */
};
其实并没有,采用联合体,obj可以当作一个指针指向实际的空间(这种技巧至于C++这种非强型的语言才有的)
4.3 工作流程
伪代码不如流程图看的清晰
4.3.1 申请
4.3.2 释放
释放的空间,计算桶号,然后挂在哈希表下,挂的时候直接挂第一个位置。如下图: