内存管理是c++和其他OOP(面向对象)语言的根本区别,熟知c++的内存管理至关重要
1.分配方式简介
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
- 栈,函数内局部变量,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 堆,malloc分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个free。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
- 自由存储区,就是那些由new等分配的内存块,他和堆是十分相似的,不过它是用delete来结束自己的生命的,自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
- 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
1.1 堆栈区别
主要的区别由以下几点:
- 管理方式不同。栈系统管理,堆程序员控制,容器产生内存泄漏(memory leak)
- 空间大小不同。堆大,栈小。栈可以 设置
- 能否产生碎片不同。频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。栈是先进后出,内存永远是连续的
- 生长方向不同。堆向上生长(内存地址增加的),栈向下生长(向着内存地址减小的方向增长)
- 分配方式不同。堆:动态分配,没有静态分配。栈:静态分配,动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
- 分配效率不同。栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
1.2 常见的内存错误及其对策
- 内存分配未成功,却使用了它。应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
- 内存分配虽然成功,但是尚未初始化就引用它。赋初值
- 内存分配成功并且已经初始化,但操作越过了内存的边界。测试覆盖
- 忘记了释放内存,造成内存泄露。内存泄漏检测工具
- 释放了内存却继续使用它。用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”
1.3 有了malloc/free为什么还要new/delete?
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
new/delete:对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数
以下示例展示了malloc和new对于构造函数的是否触发
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "construct" << endl; }
virtual ~A() { cout << "destruct" << endl; }
void print() { cout << "hello world" << endl; }
};
int main()
{
{
A a;
a.print();
}
cout << "--stack--" << endl;
{
A* b = new A;
b->print();
delete b;
b = nullptr;
}
cout << "--heap with new--" << endl;
{
A* c = (A*)malloc(sizeof(A));
c->print();
free(c);
c = nullptr;
}
cout << "--heap with malloc--" << endl;
return 0;
}
输出
construct
hello world
destruct
--stack--
construct
hello world
destruct
--heap with new--
hello world
--heap with malloc--
1.4 内存耗尽怎么办
- 判断指针是否为NULL,如果是则马上用return语句终止本函数
- 判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行
- 为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。
2. 内存管理升级 tcmalloc
tcmalloc就是一个内存分配器,管理堆内存,主要影响malloc和free,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存碎片。glibc中的内存分配器是ptmalloc2,tcmalloc号称要比它快
2.1 优化点
- tcmalloc特别对多线程做了优化,对于小对象的分配基本上是不存在锁竞争,而大对象使用了细粒度、高效的自旋锁(spinlock)
- 分配给线程的本地缓存,在长时间空闲的情况下会被回收,供其他线程使用,这样提高了在多线程情况下的内存利用率,不会浪费内存,而这一点ptmalloc2是做不到的
- tcmalloc为每个线程分配了一个线程局部的cache,线程需要的小对象都是在其cache中分配的,由于是thread local的,所以基本上是无锁操作(在cache不够,需要增加内存时,会加锁)
- tcmalloc维护了进程级别的cache,所有的大对象都在这个cache中分配,由于多个线程的大对象的分配都从这个cache进行,所以必须加锁访问
在实际的程序中,小对象分配的频率要远远高于大对象,通过这种方式(小对象无锁分配,大对象加锁分配)可以提升整体性能。
线程级别cache和进程级别cache实际上就是一个多级的空闲块列表(Free List)。一个Free List以大小为k bytes倍数的空闲块进行分配,包含n个链表,每个链表存放大小为nk bytes的空闲块。在tcmalloc中,<=32KB的对象被称作是小对象,>32KB的是大对象。在小对象中,<=1024bytes的对象以8n bytes分配,1025<size<=32KB的对象以128n bytes大小分配,比如:要分配20bytes则返回的空闲块大小是24bytes的,这样在<=1024的情况下最多浪费7bytes,>1025则浪费127bytes。而大对象是以页大小4KB进行对齐的,最多会浪费4KB - 1 bytes。