在 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)范围内的所有对象析构掉。
#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 标准。
#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()
。
/*******************第一级内存配置器模板***********************/
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) 终止程序。
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 容器就是用来保存对象的,这倒好,对象还没保存之前,已经占据了额外的内存空间了。那么,有方法解决吗?当然有!
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 的工作原理了。附上第二级配置器的部分实现内容源码:
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 重新填充空间。
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。
//模板中成员的定义,用于重新给链表分配空间
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); // 返回首节点
}
标签:__,malloc,STL,void,配置,list,alloc,free,空间 From: https://www.cnblogs.com/love-9/p/18149442