上篇文章点击打开链接主要对于对象的构造含和析构进行了主要说明,这篇文章将对对象构造前的内存配置和对象析构后的空间释放进行深入探索。
好的,话不多说马上进入是正文:
对对象构造前的内存配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI对此的设计哲学如下:
向system heap要求空间。
考虑多线程的状态
考虑内存不足的应变措施
考虑过多的“小型区块”可能造成的内存碎片问题
C++的内存配置的基本操作是:operator new(),内存释放的基本操作是operator delete。这两个全局函数相当于C的malloc和free函数,正是如此,SGI正是以malloc() 和free()
完成内存的配置与释放。
考虑到小型区块可能造成的内存破碎的问题,SGI设计了双层配置器:
第一层配置器_mallocx_alloc_template<0>malloc_alloc(typedef malloc_alloc alloc);直接使用malloc和free,
第二层配置器:_default_alloc_template<_NODE_ALLOCATOR_THREADS,0> alloc;采用不同得策略,当要配置的空间大于128bytes时,转交给第一层配置器配置,小于128bytes时,为了降低额外的负担,便采用复杂的内存池技术memory pool。
整个设计究竟只是开房第一层,还是同时开放第二层,取决于——USE_MALLOC,是否被定义。alloc不接受任何template型别的参数。
为了使配置器的接口符合STL规格,SGI为他包装了一个接口,simple_alloc;
SGI STL提供两级空间配置器,第一级空间配置器使用malloc/free函数,当分配的空间大小超过128 bytes的时候使用第一级空间配置器;第二级空间配置器使用了内存池技术,当分配的空间大小小雨128 bytes的时候,将使用第二级空间配置器。
大量分配小块的内存空间会带来问题:一是从运行库的堆管理器中取得的内存(比如通过malloc获得的内存),会有一部分空间用于存储管理信息,用于管理各个内存块,这样内存的使用率就降低了;二是过多的小块内存会带来内存碎片问题;采用合适的内存池技术可以避免这些问题。
SGI STL的第一级内存配置器就是简单的使用malloc和free,realloc,他还有对于malloc,realloc的out_of_memory处理机制(oom_malloc,oom_realloc),就是当内存分配失败的时候的一些处理机制,
oom_malloc不断的进行尝试释放,配置,释放配置......这个释放操作其实是自己写的一个处理函数,set_malloc_handler()(仿造set_new_handler(),以operator new配置的内存才可以,使用set_new_handler处理机制。),一般的处理原则是使得更多的内存空间可用。
SGI STL的第二级内存配置器维护了一个free-list数组,分别用于管理8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,free-list的节点结构如下:
union obj { union obj* free_list_link; char client_data[1]; };
这里使用union结构,是为了节省空间,也就是说,当节点位于free-list时,通过free_list_link指向下一块内存,而当节点取出来分配给用户使用的时候,整个节点的内存空间对于用户而言都是可用的,这样在用户看来,就完全意识不到free_list_link的存在,可以使用整块的内存了。
在分配内存时,会将大小向上调整为8的倍数,因为free-list中的节点大小全是8的倍数。
enum {_ALIGN = 8}; enum {_MAX_BYTES = 128}; enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN // 将待分配的空间大小向上调整为8的倍数 static size_t _S_round_up(size_t __bytes) { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); } __PRIVATE: union _Obj { union _Obj* _M_free_list_link; char _M_client_data[1]; /* The client sees this. */ }; private: // 定义free_list数组 static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS]; // 根据空间大小取得free_list数组的对应下标 static size_t _S_freelist_index(size_t __bytes) { return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1); }
空间的分配:就是从对应的free-list节点链表中取出一个节点返回给用户,当然,如果没有可用的节点的话就要通过refill来分配新的节点了,后面会有描述:
static void* allocate(size_t __n) { void* __ret = 0; // 如果大于128 bytes,则使用第一级空间配置器 if (__n > (size_t) _MAX_BYTES) { __ret = malloc_alloc::allocate(__n); } else { // 通过大小取得free-list数组下标,随后取得对应节点的指针 // 相当于&_S_free_list[_S_freelist_index(__n)] _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); _Obj* __RESTRICT __result = *__my_free_list; // 如果没有可用的节点,则通过_S_refill分配新的节点 if (__result == 0) __ret = _S_refill(_S_round_up(__n)); else { // 将当前节点移除,并当做结果返回给用户使用 *__my_free_list = __result -> _M_free_list_link; __ret = __result; } } return __ret; };
而空间的回收,则是把内存重新加入到free-list对应的节点链表上去。
那么,当对应的free-list链表中没有可用节点的时候,refill进行了怎样的操作呢?默认操作时通过_S_chunk_alloc从内存池中取得20个新的节点添加到free-list链表中,当然,内存池中的内存不够用也是会出现的情况之一,这时候能分多少就分多少节点,再万一内存池一个节点都提供不了了,那就内存池需要新增空间了,如果失败,再抛出bad_alloc异常。
template <bool __threads, int __inst> void* __default_alloc_template<__threads, __inst>::_S_refill(size_t __n) { int __nobjs = 20; // 通过内存池分配内存,第二个参数为传引用方式 char* __chunk = _S_chunk_alloc(__n, __nobjs); _Obj* __STL_VOLATILE* __my_free_list; _Obj* __result; _Obj* __current_obj; _Obj* __next_obj; int __i; // 如果只分配了一个节点,那么直接返回给用户就是了 if (1 == __nobjs) return(__chunk); // 如果分配了不止一个节点,那么多余的我们要放到free-list里面去 __my_free_list = _S_free_list + _S_freelist_index(__n); /* Build free list in chunk */ __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) { // 最后一个节点的_M_free_list_link指针指向NULL,并跳出循环 __current_obj -> _M_free_list_link = 0; break; } else { __current_obj -> _M_free_list_link = __next_obj; } } return(__result); }
总结一下:
SGI STL提供两级空间配置器,第一级空间配置器使用malloc/free函数,当分配的空间大小超过128 bytes的时候使用第一级空间配置器;第二级空间配置器使用了内存池技术,当分配的空间大小小雨128 bytes的时候,将使用第二级空间配置器。第二级内存配置器维护了一个free-list数组,分别用于管理8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128
bytes的小额区块,进行小区块的内存分配,具体内容可以参考《STL源码剖析》;