C++内存管理技术
C++ membery primitives
分配 | 释放 | 类别 | 可否重载 |
---|---|---|---|
malloc | free | C 函数 | 不可 |
new | delete | C++ 表达式 | 不可 |
::operator new | ::operator delete | C++ 函数 | 可 |
allocator< T >::allocate | allocator< T >::deallocate | C++ 标准库 | 可自由设计以搭配任何容器 |
- 第四种方法在不同的编译版本上有着不同的使用方法
#ifdef _MSC_VER
int *p4 = allocator<int>().allocate(3, (int*)0);
allocator<int>().deallocate(p4, 3);
#endif
#ifdef __BORLANDC__
int *p4 = allocator<int>().allocate(5);
allocator<int>().deallocate(p4, 5);
#endif
#ifdef __GNUC__
// 在早期的 GUNC 中使用这种调用方法
void *p4 = alloc::allocate(512);
alloc::deallocate(p4, 512);
// 在后期的 GUNC 中进行了进一步的优化和区分
int *p4 = allocator<int>().allocate(7);
allocator<int>().deallocate((int*)p4, 7);
// 而早期的 alloc 仍然十分重要,于是换成了这种写法
void *p5 = __gnuc_cxx::__pool_alloc<int>().allocate(9);
__gnuc_cxx::__pool_alloc<int>().deallocate((int*)p5, 9);
#endif
new/delete expression
new 会先分配内存,然后调用构造函数,所以 new 的具体实现如下,而平时重载的 new,都是重载了 ::operator new
Complex *pc = new Complex(1, 2);
↓↓
try {
void *mem = operator new(sizeof(Complex));// operator new 的实现调用了 malloc
pc = static_cast<Complex*>(mem);
pc->Complex::Complex(1, 2);
// 在 GCC 中用户无法直接使用构造函数
} catch(std::bad_alloc) {
}
delete pc;
↓↓
pc->Complex::~Complex();
operator delete(pc); // operaotr delete 的实现调用了 free
array new/delete
通过 new 开辟一个数组,在数组的起始位置会分配一个 4 字节的 cookie,在 cookie 里面记录了这次分配数组的长度。如果使用 delete 释放,只会释放其中的一块,其他块就发生内存泄漏,而使用 delete[] 释放,就可以保证申请的内存完全被释放了。
对于数组的构建和删除的过程中,按 0、1、2 下标顺序调用构造函数,按 2、1、0 下表顺序调用析构函数。
placement new
placement new 允许我们将对象构造于一个已经分配的内存中
char *buf = new char[sizeof(Complex)*3];
Complex *pc= new(buf)Complex(1, 2);
↓↓
try {
void *mem = operator new(sizeof(Complex), buf);
// 和 new 不同的地方在于多传了 buf 参数,于是 operator new 什么也不做直接返回 buf
pc = static_cast<Complex*>(mem);
pc->Complex::Complex(1, 2);
} catch(std::bad_alloc) {
}
class allocate
malloc 在分配内存的首尾分别会有 4 个字节的 cookie,记录了分配的内存的大小。所以频繁的 malloc 会造成很大一部分的内存浪费。
解决这种方法的技术是通过内存池技术,我可以一次性分配一大块的内存,在后续程序分配内存过程中,从这一大块中分配出一部分以供程序使用。
- 第一版的内存管理技术如下
class Screen{
public:
static void* operator new(size_t size) {
Screen *p;
if(!freeStore){ // 如果没有可以用的内存空间,则再分配一次
size_t chunk = screenChunk * size;
freeStore = p = reinterpret_cast<Screen*> (new char[chunk]); // char 字节为 1
for(; p != &freeStore[screenChunk - 1]; ++ p){
p -> next = p + 1; // 将得到的内存串成链表
}
p -> next = NULL;
}
p = freeStore;
freeStore = freeStore -> next;
return p;
}
static void operator delete(void *p) {
(static_cast<Screen*>(p))->next = freeStore;
// 显然,把 delete 掉的内存空间直接插入到头部,是最快的插入方式
freeStore = static_cast<Screen*>(p);
}
private:
Screen *next;
static Screen *freeStore; // 将可用内存用链表形式存储,存下链表头指针
static const int screenChunk; // 选择一次性分配的内存空间
private:
int i;
/* 在第二版中,使用如下方法
union {
int i;
Screen *next;
}
因为 next 只有在没有使用的时候才有意义,i 只有在被使用的时候才有意义,所以可以使用 union 来节省内存的消耗
*/
};
Screen* Screen::freeStore = NULL;
const int Screen::screenChunk = 24;
int main() {
// freopen("in", "r", stdin);
Screen *p[100];
for(int i=0; i<100; i++) {
p[i] = new Screen;
}
cout << "size of Screen " << sizeof(Screen) << endl;
for(int i=0; i<10; i++) {
cout << "address of p[" << i << "] " << p[i] << endl;
}
return 0;
}
- 第二版本和第一版本类似,只是加上了 union 优化。
- 前两个版本的分配内存操作都是在对象内实现的,然而对于每一个对象都写一个内存分配显然不符合实际,所以第三版本就是将内存分配的过程提取出来,单独封装成一个类 allocate,在每个类内存包含这个 allocate,然后调用 allocate 的函数完成分配过程。
new handler
当 operator new 没有能力分配足够的内存时,会抛出一个 bad_alloc 异常。但在抛出异常之前,其实系统会调用你设定了 new_handler 函数,这是 C++ 平台给程序员提供的一种方式。
typedef void(*new_handler);
new_handler set_new_handler(new_handler p) throw();
优秀的 new handler 应该完成两件事:
- 让更多的内存可用
- 调用 abort 或 exit 放弃操作,结束程序。
std::allocator
std::allocator 运行模式
allocator 维护 16 个链表 free_list[16],每个链表维护一个固定大小的内存池,从 free_list[0] 维护 8 字节开始,每个的链表比上一个链表多维护 8 个字节。当应用程序想要分配一个大小的内存,会向上调整到 8 的整数,然后从链表中分配。
allocator 每次会分配 size * 20 * 2 + RoundUp() 的内存,前 20 个块给链表,后面的一半内存称为备用池 pool,就是多分配出的一部分等着给下一次分配使用,从 pool 切出来的数量永远控制在 1-20 之间。RoundUp 是随着轮次的增多,每次多分配的内存,计算方式是 累计申请量>>4,然后调到 8 的倍数。
分配过程通过例子来解释:假设系统内存为 10000
- 分配 32 字节,此时 free_list[3] 为空且 pool 为空,所以会通过 malloc 一次分配 32 * 20 * 2 + RoundUp(0>>4) 字节。然后从中切出第一块给客户,剩下的 19 个块挂到 free_list[3] 上。后面的 *2 操作就是给 pool 的大小。
- 累计申请量:1280,pool 大小:640
- 分配 64 字节,此时 free_list[7] 为空但存在 pool,所以直接把 pool 分配给它用,切出第一块给客户,剩下 9 块挂到 free_list[7] 上。
- 累计申请量:1280,pool 大小:0
- 分配 96 字节,此时又重复第一次的过程,分配 96 * 20 * 2 + RoundUp(1280>>4) 字节并拿出前 20 块给 free_list[11] 使用。
- 累计申请量:5200,pool 大小:2000
- 分配 88 字节,此时 free_list[10] 为空但存在 pool,所以从 pool 分配,因为从 pool 每次分配的块数在 1-20 之间,也就是分配了 20 个出来。
- 累计申请量:5200,pool 大小:240
- 连续申请 3 个 88 字节,因为 free_list[10] 里面还有 19 个空闲,所以直接分配 3 个给客户
- 累计申请量:5200,pool 大小:240
- 申请 8 字节,此时 free_list[0] 为空但存在 pool,所以直接从 pool 分配 20 块 8 字节给 free_list[0] 使用
- 累计申请量:5200,pool 大小:80
- 申请 104 字节,此时 free_list[12] 为空但 pool 不够分配一块,那么把 80 的 pool 分配给 free_list[9],在通过 malloc 分配 104 * 20 * 2 + RoundUp(5200>>4) 字节
- 累计申请量:9688,pool 大小:2408
- 申请 112 字节,此时 free_list[13] 为空但存在 pool,所以直接从 pool 分配20 块挂到 free_list[13] 上
- 累计申请量:9688,pool 大小:168
- 申请 48 字节,此时 free_list[5] 为空但存在 pool,所以直接从 pool 分配 3 个挂到 free_list[5] 上
- 累计申请量:9688,pool 大小:24
- 申请 72 字节,此时 free_list[8] 为空但 pool 不够分配一块,那么先把 24 字节的 pool 挂到 free_list[2] 上,然后又要通过 malloc 分配。显然此时系统已经没有多余的内存完成分配,那么系统会从已经分配的最接近 72 的资源里面分配一块给客户使用,也就是说此时会从 free_list[9] 分配一块 80 字节的块并切成 72+8 的两部分,第一部分分配给客户,第二部分变成 pool。
- 累计申请量:9688,pool 大小:8
- 申请 120 字节,此时 free_list[14] 为空但 pool 不够分配一块,那么先把 8 字节的 pool 挂到 free_list[0] 上,显然此时 malloc 仍然不够内存使用,但 free_list[14] 和 free_list[15] 都是空的,那么就办法分配了。
- 累计申请量:9688,pool 大小:0
allocator 第二级配置器源码
enum {__ALIGN = 8}; // 小区块的上调边界
enum {__MAX_BYTES = 128}; // 小区块的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // free-list 的个数
class alloc {
private:
union obj{
union obj* free_list_link;
};
static char* start_free; // 指向 pool 的头
static char* end_free; // 指向 pool 的尾
static size_t heap_size; // 累计分配量
static obj* free_list[__NFREELISTS]; // free_list 数组
static size_t ROUND_UP(size_t bytes) { // 将 bytes 向上调整到 8 的倍数
return ((bytes + __ALIGN-1)) & ~(__ALIGN-1);
}
static size_t FREELIST_INDEX(size_t bytes) { // 获得应该从哪个 free_list 分配内存
return (bytes+__ALIGN-1)/__ALIGN -1;
}
static void* refill(size_t n) { // 为链表填充内存,此时的 n 已经是 8 的倍数
int nobjs = 20;
char *chunk = chunk_alloc(n, nobjs);
obj **my_free_list;
obj *result;
obj *current_obj;
obj *next_obj;
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(int i=1; ; i++) {
current_obj = next_obj;
next_obj = (obj*)((char*)next_obj+n);
if(nobjs-1 == i) {
current_obj->free_list_link = NULL;
break;
} else {
current_obj->free_list_link = next_obj;
}
}
return result;
}
static char* chunk_alloc(size_t size, int &nobjs){ // 分配一大块内存
char *result;
size_t total_bytes = size*nobjs;
size_t bytes_left = end_free - start_free;
if(bytes_left >= total_bytes) { // 先从 pool 池分配,如果能够满足分配 20 个块
result = start_free;
start_free += total_bytes;
return result;
} else if(bytes_left >= size) { // 如果不够 20 个但至少能分配一个
nobjs = bytes_left/size;
total_bytes = size*nobjs;
result = start_free;
start_free += total_bytes;
return result;
} else { // pool 池不够用
size_t bytes_to_get = 2*total_bytes+ROUND_UP(heap_size>>4);
if(bytes_left > 0) { // 处理 pool 池碎片
obj **my_free_list = free_list + FREELIST_INDEX(bytes_left);
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
start_free = (char*)malloc(bytes_to_get); // 先分配到 pool 池中
if(0 == start_free) { // 如果系统内存不够分配
obj **my_free_list;
obj *p;
for(int i=size; i<=__MAX_BYTES; i += __ALIGN) { // 往后找一块可以拿来用的内存
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if(0 != p) { // 如果找到一块可以用的
*my_free_list = p->free_list_link;
start_free = (char*)p;
end_free = start_free+i;
return chunk_alloc(size, nobjs);// 扩充的 pool,所以在分配一次一定可以分配到
}
}
// 尝试利用一级配置器分配内存
end_free = 0;
// start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return chunk_alloc(size, nobjs); // 扩充的 pool,所以在分配一次一定可以分配到
}
}
public:
static void* allocate(size_t n) {
obj **my_free_list; // obj**
obj *result;
if(n > (size_t)__MAX_BYTES) { // 改用第一级p配置器
// return malloc_alloc::allocate(n);
}
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list; // 获得对应的链表指针
if(result == NULL) { // 如果链表为空
void *r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result->free_list_link;
return result;
}
static void deallocate(void *p, size_t n) {
obj *q = (obj*) p;
obj **my_free_list; // obj**
if(n > (size_t)__MAX_BYTES) { // 改用第一级p配置器
// malloc_alloc::deallocate(p, n);
return ;
}
my_free_list = free_list + FREELIST_INDEX(n);
q->free_list_link = *my_free_list;
*my_free_list = q;
}
};
char* alloc::start_free = NULL;
char* alloc::end_free = NULL;
size_t alloc::heap_size = 0;
alloc::obj* alloc::free_list[__NFREELISTS] = {0};