主要介绍了C++的内存管理行为,即内存分配和内存归还(operator new
和operator delete
),同时了解new_handler
和set_new_handler
。
多线程里: 内存分配在heap上,多线程环境中经常会发生竞态条件,如果没有使用合理的同步机制而采用无锁算法或者精心防止并发访问,通常容易导致对heap的破坏。
stl里并不通过new和delete管理内存,而是有自己的分配器和回收器
operator new
和operator delete
适用于单一对象,Arrays一般使用operator new []
和operator delete []
malloc的底层实现
本节参考了知乎中的一篇文章 原文链接
参考二 这篇写的很清楚
在Linux中,一般分为用户空间与内核空间,用户空间的代码不能直接操作内核空间。一般提供了一系列的系统调用(System call,内核空间与用户空间的接口)来供调用。在内存分配中一般是如下流程:
- 用户空间中调用malloc/new,申请内存
- 调用系统调用,
brk(), sbrk()
或者mmap, munmap
进行内存分配 - 内核层,调用
kmalloc, vmalloc
malloc小于128k的内存时,brk and sbrk
这个阈值可以通过mallopt()
的M_MMAP_THRESHOLD选项控制
函数原型,他俩都可以完成对内存的分配以及回收
#include<unistd.h>
int brk(void *addr);
void* sbrk(intptr_t increment);
这里提出一个概念程序间断点(program break), 系统通过调整其位置,来调整分配的内存大小,即:向高位延伸则拓展内存,向低位收缩则回收内存。
brk
brk的参数是addr,当addr参数合理、系统有足够的内存并且不超过最大值时brk()函数将数据段结尾设置为addr,即间断点设置为addr。
brk()成功返回0,失败返回-1并且设置errno值为ENOMEM
sbrk
sbrk()将程序数据空间增加increment字节。
当increment为正值时,间断点位置向后移动increment字节。同时返回移动之前的位置,相当于分配内存。当increment为负值时,位置向前移动increment字节,相当与于释放内存,其返回值没有实际意义。当increment为0时,不移动位置只返回当前位置。
注意,他们完成分配的是虚拟内存,只有当程序访问到这里,发生缺页中断的时候,内核才会真正进行物理内存分配
malloc大于128k的内存时,调用mmap
brk分配的内存需要等到高地址内存释放以后才能释放(例如,按照AB的顺序申请了内存,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因),而mmap分配的内存可以单独释放。
new_handler
set_new_handler
可以指定内存分配无法满足时候的操作nothrow new
具有局限性,仅保证new操作不抛出异常,不保证同语句其他操作不抛出异常。
what is new_handler
namespace std{
// 其实new_handler就是一个函数指针的名字,返回void,参数列表为空
typedef void (*new_handler)();
// 不抛出异常,设置全局 global new_handler
new_handler set_new_handler(new_handler p) throw();
};
当operator new
无法满足操作时,会抛出异常,但是在这之前会调用错误处理函数new_handler
(会一直调用new_handler,直到成功分配内存、返回异常、程序中断等)
实验:
void outOfMemery() {
printf("Out of Memery\n");
}
int main() {
set_new_handler(outOfMemery);
try {
int i = 0;
while (true) {
printf("------------------------%d\n", i);
int *p = new int[1000000000L];
i++;
}
} catch (bad_alloc) {
printf("OVER\n");
}
}
输出:
------------------------0
------------------------1
------------------------2
------------------------3
------------------------4
------------------------5
------------------------6
------------------------7
Out of Memery
Out of Memery
Out of Memery
Out of Memery
Out of Memery
...
后面一直输出 Out of Memery
本实验中的new_handler只是打印消息,没做任何有效处理,因此会一直调用下去.
例子
这里使用了自定义的new_handler
void f() {
printf("test\n");
exit(-1);
}
int main() {
set_new_handler(f);
try {
while (true) {
int *base = new int[100000000L];
}
} catch (bad_alloc) {
printf("BAD_ALLOC\n");
}
}
/*
test
进程已结束,退出代码-1
*/
set_new_handler(0)表示没有new_handler函数,会直接抛出异常
int main() {
set_new_handler(nullptr);
try {
while (true) {
int *base = new int[100000000L];
}
} catch (bad_alloc) {
printf("BAD_ALLOC\n");
}
}
// BAD_ALLOC
new_handler应有的的行为
operator new 无法满足内存需求时,会不断调用new_handler,直到找到足够内存、抛出异常、退出,一般应该设计new_handler如下:
- 让更多的内存可被使用,以便下次operator new可以成功
- 安装另一个new_handler, 调用其他的new_handler(通过
set_new_handler
) - 卸载new_handler, 即
set_new_handler(nullptr)
,这样下次调用operator new就会直接抛出异常。 - 不返回,结束程序, 调用
exit
或者abort
类的专属new_handler
C++中并没有原生支持类的专属new_handler,但是我们可以自己实现,函数原型如下:
class Widget {
private:
static new_handler currentHandler;
public:
static new_handler set_new_handler(new_handler p) throw();
// size_t 是默认参数,由编译器在编译器自动替换
static void *operator new(size_t sz) throw(bad_alloc);
};
new_handler Widget::currentHandler = nullptr;
Widget
是一个例子类,将其专有的new_handler声明为static,保证所有的Widget
类的new_handler都是共享的.
set_new_handler
参考std的实现模式,在类里的set_new_handler
的工作如下:
- 将类的
currentHandler
设置为指定handler - 返回之前的老handler(可以由global set_new_handler来用作恢复global new_handler处理,后面会提到)
new_handler Widget::set_new_handler(new_handler p) throw() {
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
global set_new_handler的工作原理和这个一样
NewHandlerHolder
使用RAII思想构建一个管理类NewHandlerHolder
,使用构造函数保存set_new_handler
返回的old new_handler,在离开代码块自动恢复之(利用析构函数)
class NewHandlerHolder {
public:
explicit NewHandlerHolder(new_handler nh) : handlerCache(nh) {}
~NewHandlerHolder() {
std::set_new_handler(handlerCache);
}
private:
new_handler handlerCache;
NewHandlerHolder(const NewHandlerHolder &) = delete;
NewHandlerHolder &operator=(const NewHandlerHolder &) = delete;
};
operator new
Widget::operator new的工作如下:
- 调用
std::set_new_handler
,将global new_handler
设置为Widget::currentHandler
- 调用
::operator new
(即global operator new)来分配内存.- 分配失败:调用global new_handler(已经被安装为Widget::new_handler),如果最终无法满足内存分配,会抛出bad_alloc异常,
Widget::operator new
必须恢复之前的global new_handler,然后再传播异常. - 分配成功:返回一个足够Widget对象使用的内存,
Widget::operator new
会返回一个指针,指向其内存.然后恢复之前的global new_handler
- 分配失败:调用global new_handler(已经被安装为Widget::new_handler),如果最终无法满足内存分配,会抛出bad_alloc异常,
- 为了方便的管理new_handler(需要恢复操作),使用构造函数保存
set_new_handler
返回的old new_handler,在离开代码块自动恢复之(利用析构函数)
void *Widget::operator new(size_t sz) throw(class std::bad_alloc) {
NewHandlerHolder holder(std::set_new_handler(currentHandler));
return ::operator new(sz);
// 离开代码块,holder析构,会自动重置new_handler
}
使用上面的类
void outOfMemery() {
printf("Out of Memery\n");
// abort();
}
int main() {
// 为Widget设置专属new_handler
Widget::set_new_handler(outOfMemery);
Widget *widget = new Widget; // 如果失败了会调用outOfMemery
string *str = new string; // 如果失败了会调用 global new_handler
Widget::set_new_handler(0);
Widget *widget1 = new Widget; // 如果失败会返回bad_alloc
}
使用模板来实现new_handler定制化
上面的实现方法没用泛用性,要对每个类都实现set_new_handler
和operator_new
方法,使用模板技术可以使之泛化
template<typename T>
class NewHandlerSupport {
private:
static std::new_handler currentHandler;
public:
static std::new_handler set_new_handler(std::new_handler nh) throw();
static void *operator new(std::size_t sz) throw(std::bad_alloc);
};
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler nh) throw() {
std::new_handler oldHandler = currentHandler;
currentHandler = nh;
return oldHandler;
}
template<typename T>
void *NewHandlerSupport<T>::operator new(std::size_t sz) throw(std::bad_alloc) {
NewHandlerHolder holder(std::set_new_handler(currentHandler));
return ::operator new(sz);
}
使用:令Widget继承自NewHandlerSupport
class Widget : public NewHandlerSupport<Widget> {
private:
// 类的成员,不必声明 new_handler
public:
// 不必再声明 operator new 和 set_new_handler
};
这里用到的技术:
- 虽然是模板,但是其实并没有使用到T.因为这里其实要使用的模板的一个特性:自动为每一个T生成化一份实例,所以每个不同的类都会有其自己的静态对象.
- 怪异的循环模板模式(CRTP):
class Widget : public NewHandlerSupport<Widget>
nothrow
Widget *p = new (std::nothrow) Widget;
其作用:
- 分配内存失败,会直接返回nullptr指针,不会抛出异常
- 分配成功,执行下一步操作
但是存在问题:虽然在operator new
中不会抛出异常,但是同语句的Widget
的构造函数中可能会抛出异常. 所以这个技术其实一般没被使用.
new和delete的合理替换时机
替换时机
什么时候替换默认的operator new
和operator delete
是合理的呢?常见的场景如下:
检测运用上的错误
overruns:在分配的内存区后写数据
underruns:在分配的内存区前写数据
通过自定义operator new
和operator delete
可以解决这个问题:operator new
可以预先分配超额内存,在额外空间放置byte patterns
(即签名sign). operator delete
在释放内存前检查这个空间,如果发生了overrun或者underrun,那么就可以做出对应的处理.
强化性能
现实中的内存分配需求比较多样:大块内存、小块内存、混合型内存,还有各种不同的生命周期,最后会造成内存区被分割成很多碎片。
因此可以对于内存分配进行定制化操作,可以大幅度提升性能,并且减少不必要的内存消耗。
搜集使用的统计数据
自定义的new可以帮助记录在分配内存中的关键信息,例如:分配的区块大小、是FIFO还是LIFO,运用形态等
例子
一个协助检查overrun和underrun的operator new
,这里用到了static_cast
和reinterpret_cast
,使用了int
字符sign作为检查符,然后使用了static_cast
将内存前部分存为int,使用了static_cast<Byte*>
将memory
转为Byte类型,方便在上面做移位操作,指向内存末尾,然后转为int,标记下sign
static const int sign = 0xDEADBEEF;
typedef unsigned char Byte; // 一个字节
void *operator new(size_t sz) throw(bad_alloc) {
size_t realSize = sz + 2 * sizeof(int);
void *memory = malloc(realSize);
if (!memory) throw bad_alloc();
*static_cast<int *>(memory) = sign;
*reinterpret_cast<int *>(static_cast<Byte *>(memory) + realSize - sizeof(int)) = sign;
return static_cast<Byte *>(memory) + sizeof(int);
}
缺点
- 没有实现内存分配失败后反复调用new_handler,直到内存分配成功,
- 没有内存对齐
- 未处理
size = 0
的case
内存对齐
按系统来分,有些系统没有内存对齐会发生错误,有些系统内存对齐会提高性能:如X86里面,doubles可以被对齐于任意byte边界,但是如果是8-byte对齐,其访问速度会快很多。
C++要求operator new
返回的指针有适当的对齐,malloc
返回的指针就是对齐的,但是上面的偏移过的operator new
并不保证.
Summary
合理替换new & delete
的时机:
- 为了检测运用错误
- 为了搜集动态内存分配时的统计信息
- 增加分配和归还的速度:例如定制化内存池
- 降低默认内存管理器的额外空间开销:默认的内存分配器会在每个分配区上带来额外的开销
- 弥补默认分配器中的内存对齐
- 将相关对象集中成簇:某些数据通常被连续使用,可以将其分配在连续空间,降低内存缺页的开销。那么就可以将其创建在另一个heap上,这样就可以被成簇的集中在少量的内存页上。
placement new
和placement delete
可以解决这个问题. - 获得定制行为
编写new和delete的常见规则
C++规定即使申请的内存size为0,也需要返回一个合法的指针.一个例子
void* operator new(size_t sz) throw(bad_alloc){
}
placement new & placement delete
what is placement **
注意placement new
和placement delete
有两个定义,常见定义和普遍定义。我们知道默认的operator new
的定义:
void *placement new(size_t sz) throw(bad_alloc);
定义一:除了默认参数外,还有其他参数
比如,有时候为了log在new的过程中的信息,我们添加了输出流.
void *operator new(std::size_t sz, std::ostream& os) throw(std::bad_alloc);
这就是一个placement new
,因为其除了默认参数还有其他参数.
定义二:于指定某处new
void *operator new(std::size_t sz, void *pos) throw(std::bad_alloc);
即在指定的地址(pos)执行内存分配操作.