C++ 侯捷 内存管理

C++ 的内存获取机制:

void* p1 = malloc(512);
free(p1);

complex<int>* p2 = new complex<int>;
delete p2;

void* p3 = ::operator new(512);
::operator delete(p3);

//GNUC
void* p4 = alloc::allocate(512);
alloc::deallocate(p4, 512);

//GNUC4.9
void* p5 = allocator<int>().allocate(7); //分配7个int
allocator<int>().deallocate((int*)p5, 7);

表达式 new delete

new的步骤:

  1. 申请一段指定类大小的空间:new —> operator new —> malloc
  2. 转化为对应类类型
  3. 调用类构造函数

在operator new的源码中,有个std::nothrow_t& _THROW0()参数,表示这个函数不抛异常,取而代之的是返回一个空指针,用户通过判断是否为空指针来判断是否分配成功。

array new、array delete

array new是分配一个对象数组,通常容易犯得一个错误是在delete的时候忘记在delete后面加[]导致内存泄漏的问题。

replacement new

允许我们将对象分配在已经构建的内存中

他不会进行内存分配,而是调用重载的operator new,用于返回已经分配好的内存,转型、调用构造函数。

#include<new>
char* buf = new char[sizeof(Complex)* 3];
Complex* pc = new(buf) Complex(1, 2); // replacement new!!!
Complex* pc = new Complex(1, 2); 

函数operator new()、operator delete()

重载::operator new / ::operator delete

全局重载:

inline void* operator new(size_t size){
	cout << "global new() " << endl;
	return malloc(size);
}

inline void* operator new[](size_t size){
	cout << "global new[]() " << endl;
	return malloc(size);
}

在类中重载::operator new / ::operator delete更有用(array new / array delete重载也是一样的方法):

我们重载这两个函数,是为了接管内存分配的工作,接管它了有什么用呢?很有用,比如说可以做一个内存池(这个就是之前讲STL的时候的__pool_allocator)

为什么重载的operator new是static呢?因为希望实现的这个内存池是这个类的所有对象都能使用的!

我们可以重载operator new()的前提是:每一个版本的声明都必须有独特的参数列,其中第一个参数必须是size_t,其余参数以new所制定的replacement arguments为初值。

只有在上述的重载replacement new抛出异常的时候,才会调用相应的operator delete(这个需要自己去实现),因为在重载replacement new抛出异常,那么说明内存分配不成功,但是可能已经申请好内存,那么我们应该去处理申请好的这个内存。

static allocator

new handler

=default   =delete

delete 我不要,default  使用默认的版本

分配器   allocator

分配器分配途径:allocator—>allocate—>::operator new—>malloc

我们想要把operator new / delete抽取出来形成单独的一个类allocator,是的这个类和内存分配的分配细节剥离开,这样,需要内存管理的类,就调用allocator。这就是STL中分配器的实现思路。

VC6的标准分配器

这个编译器的allocator没有任何特殊设计,单纯是调用malloc,分配是以对象元素为单位

GNU C 2.9

设计是内存池的思想。16个指针指向16条链表,每条链表分配不同大小的内存空间,从左往右每一条链表间隔8个字节。理论上是这样的,但是在代码实现的时候,是首先申请一个大的内存块,然后在大的内存块上进行切分,所以不同链表之间会有连线。

每个链表会申请nx8x20x2字节的内存空间,n代表第n条链表,20表示将内存最多切分成20个子内存块,2表示会申请2倍大小的内存用于战备。这些内存空间是cookie free的。

如果申请的内存大于128字节,那么就有malloc来管理,就不归这个pool管理了。

源码中deallocate没有free操作(源于先天设计缺陷),即只要被pool申请空间之后,就不会再还回操作系统了。对于一个程序来说,这个也没有什么大不了的,但是对于一台机器来说,不止运行一个程序,这是有弊端的。这可能就是为什么GNU4.9把它替换回去的原因?

C函数malloc、free

malloc分配出的内存块

组成:

  1. 头尾两个cookie存放当前内存块大小,是否分配出的信息
  2. debug header用于debug
  3. 真实分配的内存块

分配出的内存块大小必须是16字节的倍数,也就是16字节内存对齐。

VC6和VC10的malloc比较


CRT:C RunTime
SBH:Small Block Heap

VC6:如果申请的内存小于1016,则是SBH服务,否则,用操作系统的HeapAlloc;
VC10:全部由操作系统的HeapAlloc分配内存,但是小内存的分配工作也被整合到HeapAlloc中了。


VC6内存分配细节

SBH首次分配内存详细步骤

总体的malloc如上图。总的来说,就是为了添加debug信息,申请具体的内存,方便内存管理。

_heap_init()做的事是分配16个头,16个头的数据结构如下,使用这个数据结构使得每个header能够管理1M的内存,1M内存能够快速分配,快速回收。一个header包括两个指针,一个指针指向自己管理的内存,一个指针指向管理内存中心。详细见步骤6。这些bit用于存放链表的状态,是否挂载上page。

_ioinit()负责第一次分配内存:根据是不是debug的模式,调用不同的内存分配策略。debug直接调用malloc,非debug则分配256字节的内存:

_heap_alloc_dbg()负责在debug模式下,加上一些调试信息(debug header),这些调试信息就是下面的数据结构_CrtMemBlockHeader。这些调试信息包括:调试文件名,行号,数据大小,间隔符。绿色的地方就是填充我们最后要申请分配的内存。被分配的内存会被指针串起来形成一条链表。

_heap-alloc_base()根据申请的大小来判断(是否大于1016,为什么是1016?因为要留给cookie八字节的内存)是要自己提供服务还是交由操作体统来服务。

__sbh_alloc_block()用于字节对齐,加首尾cookie。处理完之后的内存块如下图右上角的图。cookie利用最后的一个bit来代表这个内存块是否被分配出去,1表示已分配,0表示未分配。

__sbh_alloc_new_region():内存管理中心数据结构就是tagRegion,管理区块是不是被分配,在不在内存分配链表上。为了管理好1M的内存空间,花了16K的内存(tagRegion数据结构)。

__sbh_alloc_new_group():4K一个page。一个header管理1M内存,这个1M内存会被分成32块(组,每个组由32对指针组成,即双向链表),32块分别由内存管理中心的数据结构来管理,每一个块又会被分成8个page,一个page占4K,使用链表将这些page连起来,连起来之后首尾接到绿箭头指向的双向链表中。完成这些工作之后,申请一块内存,得到内存,初始化,内存申请就成功了。

最后将__sbh_alloc_new_region组好的内存填充到组中去。层层return会让用户拿到指向绿色开头的指针。

SBH第二(n)次分配内存

在一个page上继续进行切分,当前page不够用,去其他page找。header负责维护的状态应该发生变化。

内存释放

将一个内存块的cookie中的状态置为未分配,将内存变为两个指针,指向负责管理内存的链表,header中的bit也发生变化,表示未分配。

回收相邻的内存。应该有个合并策略(这就是为什么要有下cookie的原因),要不然会出现内存碎片的问题。

free( p )


p属于哪个header?
p属于哪个group?
p属于哪个free-list?

内存分配总结


上述的分配策略,总的思想是一个分段管理。至于为什么有16个头,32个组,1个头管理1M内存,这些都是经验值,有利于操作系统。

全回收的动作会被延缓,并不会只要归还所有内存之后就把这么多段的内存整合还给操作体统(defer)。当第二个全回收出现的时候才会把内存归还操作系统。

GNU4.9分配器


分配器各有各的优势。

new_allocator


实现出简谱的operator new和operator delete,这个好处就是能够重载。

malloc_allocator


直接调用malloc和free,没有经过operator new / delete,所以没有重载的机会。

下面两种是两种智能型的分配器。

array_allocator

允许分配一个已知且固定大小的内存块,静态分配,内存来自array object。用上这个分配器,大小固定的容器就无需在调用operator new / delete。

静态分配,不需要free。没啥用,因为不free,用过的空间就不能再次使用了。

debug_allocator

相当于一个warpper,可以包裹在任何分配器之上。它把用户的申请量添加了一些,然后由分配器回应,并以额外的内存存放size信息。

下面的三种分配器实现都是内存池的思想,追求的是不要分配过多的cookie。

__mt_allocator
多线程的内存分配器。

__pool_allocator
就是GNU2.9中的标准分配器,说了好多遍了,不再赘述。

bitmap_allocator
一个高效能的分配器,使用bit-map追踪被使用和未被使用的内存块。

 

上图是一个super block
bitmap用于存放后面的block是否被分配出去
use-count表示已经分配了多少个
__mini_vector用来管理后面的block,指向block的头尾

bitmap的变化方向和_M_start变化方向相反。
如果一个super block用光了,就会新起一个super block,而且能够分配的内存会翻倍,如下图:

如果发生全回收,则可分配的内存减半。

全回收,使用另外一个__min_vector来登记(free list)全回收的super block,发生全回收,则分配内存减半,如下图是三次全回收的情况:

如果free list中已经有64个super block,下一次再free的的super block,直接delete。

好文链接:

五、C++内存管理机制 —— primitives(侯捷)_c++内存管理 侯捷-CSDN博客

 

参考文章:侯捷C++八部曲笔记(五、内存管理)_侯捷内存管理-CSDN博客