在 C++ 里,当我们调用 new 和 delete 进行对象的创建和销毁的时候,也同时会有内存配置操作和释放操作:
这其中的 new 和 delete 都包含两阶段操作:
-
对于 new 来说,编译器会先调用 ::operator new 分配内存;然后调用 Obj::Obj() 构造对象内容。
-
对于 delete 来说,编译器会先调用 Obj::~Obj() 析构对象;然后调用 ::operator delete 释放空间。
为了精密分工,STL allocator 决定将这两个阶段操作区分开来。
-
对象构造由 ::construct() 负责;对象释放由 ::destroy() 负责。
-
内存配置由 alloc::allocate() 负责;内存释放由 alloc::deallocate() 负责;
1. 构造construct和析构destroy
在 STL 里面,construct() 函数接受一个指针 P 和一个初始值 value,该函数的用途就是将初值设定到指针所指的空间上。
destroy() 函数有两个版本,第一个版本接受一个指针,准备将该指针所指之物析构掉。直接调用析构函数即可。第二个版本接受 first 和 last 两个迭代器,将[first,last)范围内的所有对象析构掉。
cpp
#include <new.h>//欲使用placement new。 //版本1 template <class T1, class T2> inline void construct(T1* p, const T2& value) { new (p) T1(value); //在分配好的内存中,重新构建对象,p指向分配好的内存,然后重新创建T1,通过T2初始化。 //并调用T1::T1(value)构造函数 } template <class T> inline void destroy(T* pointer) { pointer->~T(); } //直接调用对象里面的析构函数而已 //版本2,接受迭代器进行析构范围对象 template <class ForwardIterator> inline void destroy(ForwardIterator first, ForwardIterator last) { __destroy(first, last, value_type(first)); }
其中 destroy() 只截取了部分源码,全部实现还考虑到特化版本,比如判断元素的数值类型 (value type) 是否有 trivial destructor 等限于篇幅,完整代码请参阅《STL 源码剖析》。
2. 内存管理
考虑到小型区块可能造成的内存破碎问题,SGI 为此设计了双层级配置器。当配置区块超过 128bytes 时,称为足够大,使用第一级配置器,直接使用 malloc() 和 free()。当配置区块不大于 128bytes 时,为了降低额外负担,直接使用第二级配置器,采用复杂的 memory pool 处理方式。无论使用第一级配接器(malloc_alloc_template)或是第二级配接器(default_alloc_template),alloc 都为其包装了接口,使其能够符合 STL 标准。
cpp
#ifdef __USE_MALLOC ... typedef __malloc_alloc_template<0> malloc_alloc; typedef malloc_alloc alloc; // 令alloc为第一级配置器 #else ... typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; // 令alloc为第二级配置器 !_USE_MALLOC #endif template <class T, class Alloc> class simple_alloc { public: static T* allocate(size_t n) { return 0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T)); } static T* allocate(void) { return (T*) Alloc::allocate(sizeof(T)); } static void deallocate(T* p, size_t n) { if (0 != n) Alloc::deallocate(p, n * sizeof(T)); } static void deallocate(T* p) { Alloc::deallocate(p, sizeof(T)); } };
其中, __malloc_alloc_template
就是第一级配置器,__default_alloc_template
就是第二级配置器。
其中 SGI STL 将配置器多了一层包装使得 Alloc 具备标准接口。
2.1 一级配置器
第一级配置器以 malloc(), free(), realloc() 等 C 函数执行实际的内存配置、释放和重配置操作,并实现类似 C++ new-handler 的机制(因为它并非使用 ::operator new 来配置内存,所以不能直接使用C++ new-handler 机制)。
SGI 第一级配置器的 allocate() 和 reallocate() 都是在调用malloc() 和 realloc() 不成功后,改调用 oom_malloc()
和oom_realloc()
。
cpp
/*******************第一级内存配置器模板***********************/ template <int inst> // 非类型参数 class __malloc_alloc_template { private: //函数指针,用来处理内存不足情况。oom = out of memory static void *oom_malloc(size_t);//指针函数是一个函数 static void *oom_realloc(void *, size_t);//指针函数是一个函数 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); // 直接使用free释放空间 } 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); } };
上面代码是第一级内存分配器,其实就是简单的封装了一下malloc和free。但是代码部分必须注意,首先成员全部是静态的,那么也就说明可以直接通过类调用成员函数而无需定义对象。
oom_malloc
和oom_realloc
是指针函数,用来处理堆内存不足的情况。__malloc_alloc_oom_handler
是函数指针,相当于回调函数,用户可以通过类成员函数set_malloc_handler
设定此函数指针,当在内存分配不足的时候,此函数将被调用。
oom_malloc()
和 oom_realloc()
都有内循环,不断调用“内存不足处理例程”,期望某次调用后,获得足够的内存而圆满完成任务,如果用户并没有指定“内存不足处理程序”,这个时候便无力乏天,真的是没内存了,STL 便抛出异常。或调用exit(1) 终止程序。
cpp
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); } }
在oom_malloc
中当malloc
失败的时候,也就堆中内存不足时候,通过此函数来榨取内存,首先会调用用户注册的回调函数__malloc_alloc_oom_handler()
看是否可以释放处对应的内存,然后继续尝试malloc
。
2.2 二级配置器
简单来说 SGI第二级配置器的做法是:sub-allocation (层次架构):
2.2.1 自由链表free_list
一方面,自由链表中有些区块已经分配给了客户端使用,所以 free_list 不需要再指向它们;另一方面,为了维护 free-list,每个节点还需要额外的指针指向下一个节点。
那么问题来了,如果每个节点有两个指针?这不就造成了额外负担吗?本来我们设计 STL 容器就是用来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。那么,有方法解决吗?当然有!
cpp
union obj {//free_list节点构造 union obj * free_list_link; char client_data[1]; /* The client sees this. */ };
在 union obj 中,定义了两个字段,再结合上图来分析:
-
从第一个字段看,obj 可以看做一个指针,指向链表中的下一个节点;
-
从第二个字段看,obj 可以也看做一个指针,不过此时是指向实际的内存区。
2.2.1 实现源码
到这里,我们已经基本了解了第二级配置器中 free_list 的工作原理了。附上第二级配置器的部分实现内容源码:
cpp
template <bool threads, int inst> class __default_alloc_template { private: enum {__ALIGN = 8}; // 8字节对齐 enum {__MAX_BYTES = 128}; // 上限 enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free_list数目 static size_t ROUND_UP(size_t bytes) { return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); } // 将bytes上调至8的倍数 __PRIVATE: union obj { // free_list节点构造 union obj * free_list_link; char client_data[1]; /* The client sees this. */ }; private: static obj * __VOLATILE free_list[__NFREELISTS]; // 16个free_lists,指针数组,free_list是一个指针指向的数据类型也是指针 static size_t FREELIST_INDEX(size_t bytes) { return (((bytes) + __ALIGN-1)/__ALIGN - 1); } // 根据区块大小,决定使用第几个free_list。 static void *refill(size_t n); static char *chunk_alloc(size_t size, int &nobjs); // 配置一大块空间,可容纳n个objs的大小的size区块 static char *start_free; // 内存池起始 static char *end_free; // 内存池结束 static size_t heap_size; // 堆大小 public: static void * allocate(size_t n) { obj * __VOLATILE * my_free_list; obj * __RESTRICT result; if (n > (size_t) __MAX_BYTES) {//大于128字节,调用第一级配置器 return(malloc_alloc::allocate(n)); } my_free_list = free_list + FREELIST_INDEX(n); // 寻找16个free list中适当的一个 result = *my_free_list; // 取出链表首节点 if (result == 0) { // 如果对应链表为空,准备重新增加链表节点 void *r = refill(ROUND_UP(n)); return r; } *my_free_list = result -> free_list_link; // free_list[n]中更改为下一个节点地址 return (result); // 将第一个节点地址返回给外部使用 } // 释放内存, static void deallocate(void *p, size_t n) { obj *q = (obj *)p; obj * __VOLATILE * my_free_list; if (n > (size_t) __MAX_BYTES) { malloc_alloc::deallocate(p, n); return; } my_free_list = free_list + FREELIST_INDEX(n); // 寻找对应freelist q -> free_list_link = *my_free_list; // 调整freelist,回收区块 *my_free_list = q; } static void * reallocate(void *p, size_t old_sz, size_t new_sz); // 声明成员函数 };
2.3 标准接口函数 allocate
2.3.1 空间申请
我们知道第二级配置器拥有配置器的标准接口函数 allocate()。此函数首先判断区块的大小,如果大于 128bytes –> 调用第一级配置器;小于128bytes–> 就检查对应的 free_list(如果没有可用区块,就将区块上调至 8 倍数的边界,然后调用 refill(), 为 free list 重新填充空间。
cpp
static void * allocate(size_t n) { obj * __VOLATILE * my_free_list; obj * __RESTRICT result; if (n > (size_t) __MAX_BYTES) { // 大于128字节,调用第一级配置器 return(malloc_alloc::allocate(n)); } my_free_list = free_list + FREELIST_INDEX(n); // 寻找16个free list中适当的一个 result = *my_free_list; // 取出链表首节点 if (result == 0) { // 如果对应链表为空,准备重新增加链表节点 void *r = refill(ROUND_UP(n)); return r; } *my_free_list = result -> free_list_link; // free_list[n]中更改为下一个节点地址 return (result); // 将第一个节点地址返回给外部使用 };
NOTE:每次都是从对应的 free_list 的头部取出可用的内存块。然后对free_list 进行调整,使上一步拨出的内存的下一个节点变为头结点。
2.3.1 空间释放
同样,作为第二级配置器拥有配置器标准接口函数 deallocate()。该函数首先判断区块大小,大于 128bytes 就调用第一级配置器。小于 128 bytes 就找出对应的 free_list,将区块回收。
NOTE:通过调整 free_list 链表将区块放入 free_list 的头部。
2.3.3 重新填充 free_lists
当发现 free_list 中没有可用区块时,就会调用 refill() 为free_list 重新填充空间。
-
新的空间将取自内存池(经由 chunk_alloc() 完成);
-
缺省取得20个新节点(区块),但万一内存池空间不足,获得的节点数可能小于 20。
cpp
//模板中成员的定义,用于重新给链表分配空间 template <bool threads, int inst> void* __default_alloc_template<threads, inst>::refill(size_t n) { int nobjs = 20; // 默认每个链表含有20个节点 char * chunk = chunk_alloc(n, nobjs); obj * __VOLATILE * my_free_list; obj * result; obj * current_obj, * next_obj; int i; if (1 == nobjs) return(chunk); my_free_list = free_list + FREELIST_INDEX(n); /* 将分配的内存连接成链表 */ result = (obj *)chunk; *my_free_list = next_obj = (obj *)(chunk + n); for (i = 1; ; i++) { 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); // 返回首节点 }