整理于侯捷的c++内存管理,侵删
第一讲 primitives
直接挖一大块,然后切分成一小块一小块的,里面放置要存放的对象(减少了调用malloc的次数)
概述
当c++应用程序想获取一块内存时:
可以通过以下方式来获取内存
分配 | 释放 | 类型 | 可否重载 |
---|---|---|---|
malloc() | free() | C函数 | 不可 |
new | delete | C++表达式 | 不可 |
::operator new() | ::operator delete() | C++函数 | 可 |
allocator::allocate() | allocator::deallocate() | C++标准库 | 可自由设计并以之搭配任何容器 |
示例:
void* p1 = malloc(512); //512bytes
free(p1);
complex<int>* p2 = new complex<int>; //one object
delete p2;
void* p3 = ::operator new(512);//512bytes
::operator delete(p3);
//GNUC 2.9版本
//以下两个函数都是static,可通过全名调用之。以下分配512bytes
void* p4 = alloc::allocate(512);
alloc::deallocate(p4,512);
//GNUC 4.9版本(两种方式)
//以下两个函数都是non-static,定要通过object调用。以下分配7个ints
void* p4 = allocator<int>().allocate(7);
allocator<int>().deallocate((int*)p4,7);
//以下两个函数都是non-static,定要通过object调用。以下分配9个ints
void* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9);
__gnu_cxx::__pool_alloc<int>().deallocate((int*)p5,9);//2.9版本的alloc到4.9版本换了名字,叫__pool_alloc
new
c++给我们的三个工具:new array new placement new
Complex* pc = new Complex(1,2);
//编译器转为:
Complex *pc;
try{
void* mem = operator new(sizeof(Complex)); //allocate
pc = static_cast<Complex*>(mem); //cast
pc->Complex::Complex(1,2); //construct
//注意:只有编译器才可以想这样直接呼叫ctor,欲直接调用ctor,可运用placement new:new(p)Complex(1,2);
}
catch( std::bad_alloc ){
//若allocation失败就不执行constructor
}
void *operator new(size_t size,const std::nothrow_t&) _THROW0(){ //nothrow这个表示该函数一定不会发生异常
//try to allocate size bytes
void *p;
while((p = malloc(size)) == 0){ //直到内存分配成功
//buy more memory or return null pointer
_TRY_BEGIN
if(_callnewh(size)==0) break; //_callnewh里面定义我们自己的动作,例如:释放内存等
_CATCH(std::bad_alloc) return (0);
_CATCH_END
}
return (p);
}
delete
Complex* pc = new Complex(1,2);
...
delete pc;
//编译器转为:
pc->~Complex();//先析构
//可以直接呼叫它的析构函数
operator delete(pc);//然后释放内存
void __cdecl operator delete(void* p)_THROW0(){
//free an allocated object
free(p);
}
array new, array delete
Complex* pca = new Complex[3];
//唤起三次ctor
//无法由参数给予初值
...
delete[] pca; //delete pca
//唤起三次dtor //唤起一次dtor
在array new之后,内存中开辟额外空间存放cookie,来记录创建了多少个对象,在调用delete时会根据cookie来free掉这里面的东西,由于这个对象内部成员变量没有指针,对象的析构函数并没有作用,所以在这种情况下,使用delete和delete[ ]没有区别。
在array new包含指针的对象后,使用delete将只唤起一次dtor,指针由于有cookie可以全部释放,但是指针指向的对象只调用了一次析构函数,只释放了一个,造成内存泄漏,所以,在使用array new的时候都建议使用array delete来释放
总结:
- 对class without ptr member 可能没有影响
- 对class with pointer member 通常有影响
两张重要的截图:
若创建的是对象,并且对象的析构函数有意义,那么它就会多一块位置来存放它创建的个数,若使用delete,发现内存中多了一个3,导致整个结构出现混乱,在调试模式下报错
placement new
定义:placement new允许我们将object建构于已分配的内存中
#inclide<new>
char* buf = new char[sizeof(Complex)*3];
Complex* pc = new(buf)Complex(1,2);//placement new
...
delete[] buf
//第三行编译器转为:
Complex* pc;
try{
void* mem = operator new(sizeof(Complex),buf); //allocate 其实这一步不做事
pc = static_cast<Complex*>(mem); //cast
pc->Complex::Complex(1,2); //construct
}
catch( std::bad_alloc ){
//若allocation失败就不执行constructor
}
void* operator new(size_t,void* loc)
{ return loc; }
重载
c++应用程序,分配内存的途径:
c++容器,分配内存的途径:
重载::operator new/::operator delete
//全局的,影响很大,不常这么做
void* myAlloc(size_t size)
{ return malloc(size); }
void myFree(void* ptr)
{ return free(ptr); }
//它们不可以被声明于一个namespace内
inline void* operator new(size_t size)
{ return myAlloc(size); }
inline void* operator new[](size_t size)
{ return myAlloc(size); }
inline void* operator delete(void* ptr)
{ return myFree( ptr ); }
inline void* operator delete[](void* ptr)
{ return myFree( ptr ); }
------------正式版--------------
void *operator new(size_t size,const std::nothrow_t&) _THROW0(){ //nothrow这个表示该函数一定不会发生异常
//try to allocate size bytes
void *p;
while((p = malloc(size)) == 0){ //直到内存分配成功
//buy more memory or return null pointer
_TRY_BEGIN
if(_callnewh(size)==0) break; //_callnewh里面定义我们自己的动作,例如:释放内存等
_CATCH(std::bad_alloc) return (0);
_CATCH_END
}
return (p);
}
void __cdecl operator delete(void* p)_THROW0(){
//free an allocated object
free(p);
}
重载operator new/operator delete
通常在类中重载这两个函数来实现内存管理(最有价值)
class Foo{
public:
(static) void* operator new(size_t);
(static) void operator delete(void*,size_t /*optional*/);
//这两个函数必须设为静态,因为调用这两个函数的时机是在创建对象完成前,由于必须是静态,c++自动帮我们写了,所以我们可以不用写
}
重载operator new[ ]/operator delete[ ]
class Foo{
public:
void* operator new[](size_t);
void operator delete[](void*,size_t /*optional*/);
}
重载new()/delete()
默认里面放的是一个已经分配好内存的指针
我们可以重载多个版本的 class member operator new(),前提是每一个版本都必须有独特的参数列表,其中第一参数必须是size_t,其余参数以new所指定的 placement arguments 为初值。出现于new(…)小括号内的便是所谓的 placement arguments
class Foo{
public:
Foo(){ }
Foo(int){ throw Bad(); }
//1、这就是一般的operator new()的重载
void* operator new(size_t size) {
return malloc(size);
}
//2、标准库提供的placement new()的重载
void* operator new(size_t size,void* start) {
return start;
}
//3、崭新的placement new
void* operator new(size_t size,long extra) {
return malloc(size+extra);
}
//4、崭新的placement new
void* operator new(size_t size,long extra,char init) {
return malloc(size+extra);
}
//一般需要编写对应的重载的operator new的operator delete,目的是:当我们的构造函数出错时,由这个来释放已经分配好的内存,例如3 这里分配了额外的内存,就需要额外的释放
//new对象-->operator new分配内存-->调用构造函数-->出错-->operator delete
void operator delete(void*,size_t)
{ }
void operator delete(void*,void*)
{ }
void operator delete(void*,long)
{ }
void operator delete(void*,long,char)
{ }
}
在标准库的string中,重载了new(),因为它申请的内存为字符串大小+1+一包东西
实例1:Per-class allocator
#inclide<cstddef>
#include<iostream>
using namespace std;
class Screen {
public:
Screen(int x):i(x){};
int get() { return i; }
void* operator new(size_t);
void operator delete(void*,size_t);
private:
Screen* next;
static Screen* freeStore;
static const int screenChunk;
int i;
};
Screen* Screen::freeStore=0;
const int Screen::screenChunk=24;
void* Screen::operator new(size_t size)
{
Screen *p;
if (!freeStore)
{
//linked list是空的,所以申请一大块
size_t chunk=screenChunk* size;
freeStore=p=reinterpret_cast<Screen*>(new char[chunk]);
for(;p!=&freeStore[screenChunk-1];++p)
p->next=p+1;
p->next=0;
}
p=freeStore;
freeStore=freeStore->next;
return p;
}
void Screen::operator delete(void* p,size_t)
{
//将deleted object插回free list前端
(static_cast<Screen*>(p))->next=freeStore;
freeStore=static_cast<Screen*>(p);
}
int main()
{
size_t const N = 100;
Screen* p[N];
for (int i = 0; i < N; ++i)
p[i] = new Screen(i);
for (int i = 0; i < 10; ++i)
cout << p[i] << endl;
for (int i = 0; i < N; ++i)
{
delete p[i];
}
return 0;
}
实例2:Per-class allocator2
使用到了嵌入式指针,节省指针使用的字节
class Airplane {
private:
struct AirplaneRep{
unsigned long miles;
char type;
};
private:
union
{
AirplaneRep rep;
Airplane* next;
};
public:
void set(unsigned long m, char t) {
rep.miles = m;
rep.type = t;
}
public:
static void* operator new(size_t size);
static void operator delete(void* deadObject, size_t size);
private:
static const int BLOCK_SIZE;
static Airplane* headOfFreeList;
};
Airplane* Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;
void* Airplane::operator new(size_t size)
{
//如果大小有误,转交给 ::operator new() 当发生继承时会出错
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane* p = headOfFreeList;
if (p) //如果p有效,就把list头部下移一个元素
headOfFreeList = p->next;
else {
//free list 已空,申请一大块
Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));
//将小块串成一个free list,
//但跳过#0,因它将被传回做为本次成果
for (int i = 1; i < BLOCK_SIZE - 1; ++i)
newBlock[i].next = &newBlock[i + 1];
newBlock[BLOCK_SIZE - 1].next = 0;//结束list
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
void Airplane::operator delete(void* deadObject, size_t size)
{
if (deadObject == 0) return;
if (size != sizeof(Airplane)) {
::operator delete(deadObject);
return;
}
Airplane* carcass = static_cast<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
int main()
{
size_t const N = 100;
Airplane* p[N];
for (int i = 0; i < N; ++i)
p[i] = new Airplane;
p[1]->set(1000, 'A');
p[5]->set(2000, 'B');
for (int i = 0; i < 10; ++i)
cout << p[i] << endl;
for (int i = 0; i < N; ++i)
delete p[i];
}
实例3:static allocator
#include<iostream>
using namespace std;
//将具体动作封装为一个类
class Allocator
{
private:
struct obj
{
struct obj* next;
};
public:
void* allocate(size_t);
void deallocate(void*, size_t);
private:
obj* freeStore = nullptr;
const int CHUNK = 5;
};
void* Allocator::allocate(size_t size)
{
obj* p;
if (!freeStore) {
//linked list为空,于是申请一大块
size_t chunk = CHUNK * size;
freeStore = p = (obj*)malloc(chunk);
//将分配得来的一块当做linked list般,小块小块串接起来
for (int i = 0; i < (CHUNK - 1); ++i) {
p->next = (obj*)((char*)p + size);
p = p->next;
}
p->next = nullptr;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
void Allocator::deallocate(void* p, size_t size)
{
((obj*)p)->next = freeStore;
freeStore = (obj*)p;
}
//直接调用分配的类即可
class Foo {
public:
long L;
string str;
static Allocator myAlloc;
public:
Foo(long l) :L(l){}
static void* operator new(size_t size)
{
return myAlloc.allocate(size);
}
static void operator delete(void* pdead,size_t size)
{
return myAlloc.deallocate(pdead,size);
}
};
Allocator Foo::myAlloc;
int main()
{
Foo* p[100];
for (int i = 0; i < 23; ++i) {
p[i] = new Foo(i);
cout << p[i] << " " << p[i]->L << endl;
}
for (int i = 0; i < 23; ++i) {
delete p[i];
}
return 0;
}
补充
New Handler
当operator new没能力为你分配出你所申请的memory,会抛出一个std::bad_alloc exception。某些老的编译器则是返回0(所以建议在分配内存后判断一下指针是否为NULL),你也可以让编译器这么做:
new(nothrow) Foo;
//让编译器不抛出异常,而是返回0
来看编译器默认operator new的代码
void *operator new(size_t size,const std::nothrow_t&) _THROW0(){
//try to allocate size bytes
void *p;
while((p = malloc(size)) == 0){ //当内存不够用时会发生异常,但是在抛出异常前会进入到_callnewh函数
//buy more memory or return null pointer
_TRY_BEGIN
if(_callnewh(size)==0) break; //该函数可以调用一个可由client指定的handler
_CATCH(std::bad_alloc) return (0);
_CATCH_END
}
return (p);
}
//new handler的形式和设定方法
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p)throw();
设计良好的new handler只有两个选择
- 让更多的memory可用
- 调用abort()或exit()
#include<new>
#include<iostream>
#include<cassert>
void noMoreMemory() //new handler
{
cerr<<"out of memory";
abort();
}
void main()
{
set_new_handler(noMoreMemory);//设置进去
}
//在new handler中要么分配内存,使先前的while能正确分配内存后推出循环,要么abort()并返回一些提示讯息,如果不调用abort(),那么while循环永远不会退出
=default,=delete
//static void* operator new(size_t size) = default; //default只能出现在big five中
static void* operator new[](size_t size) = delete;
Foo* pF = new Foo[10]; //error
第二讲 std::allocator
(减少了大量cookie)
VC6、BC5和G2.9标准分配器的实现(是给容器用的)
在vc6下和borland5下一样,标准分配器allocator里面直接调用默认的operator new和operator delete,并未做任何改动
在gnu gcc2.9版下,它的默认标准分配器为alloc,这个分配器就做到了去除cookie,但是在4.9版的时候被改名为__poll_alloc,并且不是默认的分配器,4.9版的默认分配器被改成和vc6下的一样了
//在gcc4.9下如果想使用好的那个分配器
vector<int,__gnu_cxx::__pool_alloc<int>> vecPool;
//一般容器装的是同样大小的对象,所以没必要每份都带有cookie
alloc运作模式分析
- step1:alloc内部维护了一个长为16的指针链表,每一根指针挂载的大小为(n+1)*8,如0号指针下面挂载的是开辟容器大小空间为8字节的链表(如开辟了vector,因为double是8字节,所以挂载到0号指针下)
- step2:申请32bytes,由于pool为空,故索取并成功向pool注入32 * 20 * 2 + RoundUp(0>>4)=1280,从中切出一块返回给顾客,19块给list#3,余640备用(累计申请量:1280,pool大小:640),pool由start_free和end_free两根指针维护
- step3:申请64bytes,由于pool有余量,故取pool割分为640/64=10个区块,第一块给客户,余9个挂于list#7(累计申请量:1280,pool大小:0)
- step4:申请96bytes,由于pool无余量,故索取并成功向pool注入96 * 20 * 2+Roundup(1280>>4)。其中19个区块给list#11,1个返回给客户,余2000备用(累计申请量:5200,pool大小:2000)
- step5:申请88bytes,由于pool有余量,故取pool割分为20个区块,第一块给客户,余19个挂于list#10,pool余量:2000-88*20=240(累计申请量:5200,pool大小:240)
- step6:连续三次申请88.直接由list#10取出返回给客户(累计申请量:5200,pool大小:240)
- step7:申请8bytes,由于pool有余量,故取pool割分为20个区块,第一块给客户,余19个挂于list#0(累计申请量:5200,pool大小:80)
- step8:申请104,list#12无区块,pool余量又不足供应1个,于是先将pool余额给list#9(碎片处理),然后索取并成功获得注入104 * 20 * 2+Roundup(5200>>4),切出19个给list#12,最前头那个返回给客户,余2408备用(Roundup函数会调整申请的内存的大小,向上调整为8的倍数)(计申请量:9688,pool大小:2408)
- step9:申请112bytes,由于pool有余量,故取pool割分为20个区块,第一块给客户,余19个挂于list#13(累计申请量:9688,pool大小:168)
- step10:申请48bytes,由于pool有余量,故取pool割分为3个区块,第一块给客户,余2个挂于list#5(累计申请量:9688,pool大小:24)
- step11:修改内存大小为10000(模拟分配不了的情况),申请72bytes,list#8无可用区块,pool余量又不足供应一个,于是先将pool余额给list#2,然后索取72 * 20 *2+Roundup(9688>>4),但无法满足此次索取,于是alloc从手中资源最接近80(list#9)回填pool,再从中切出72返给客户,余8(累计申请量:9688,pool大小:8)
- step12:申请72bytes,list#8无可用区块,pool余量又不足供应一个,于是先将pool余额给list#0,然后索取72 * 20 *2+Roundup(9688>>4),但无法满足此次索取,于是alloc从手中资源最接近88(list#10)回填pool,再从中切出72返给客户,余16(累计申请量:9688,pool大小:16)
- step13:申请120bytes,list#14无可用区块,pool余量又不足供应一个,于是先将pool余额给list#1,然后索取120 * 20 *2+Roundup(9688>>4),但无法满足此次索取,于是alloc从手中资源取最接近者回填pool,但是找不到,于是失败(累计申请量:9688,pool大小:0)
检讨
-
alloc手中还有多少资源?以上白色皆是。可不可以将白色的小区块合成为较大区块供应给客户?技术难度极高
-
system heap中还剩多少资源?10000-9688=312.可不可以将失败的那次索取折半…再折半…最终索取量<=312便能获得满足
第三讲 malloc/free
运作模式分析
只有在申请的内存小于1016字节(1024-8(cookie))使用sbh分配内存,大于则直接由操作系统分配,接下来讨论的是由sbh的分配过程
_heap_init()
先调用操作系统函数获取一块内存,大小为4096(无实际意义,后面不够了操作系统会把它增长)
_sbh_heap_init()
从先前分配好的内存中取16个header大小的内存,并用指针指向这16个header(每一个header管理1M内存,这1M只是虚拟地址空间)
header的结构:
_ioinit()
程序的第一次内存分配(256字节)
第一步先判断当前是否在debug模式,如果是则进入分配debug内存的函数,我们讨论debug
_heap_alloc_dbg()
在要分配的内存头尾填上4*8+4,便于debug(当前只是在调整大小,并未正式分配)
_heap_alloc_bace()
判断前面计算好的内存大小是否大于1016,如果大于1016,就直接交给操作系统;小于等于1016则继续进行
__sbh_alloc_block
添加上头尾cookie8字节,并调整为16的倍数(cookie存储的为16进制,最后四位永远为0,所以借用最后一位为1的时候代表该块的分配出去的,为0为回收的)
以上都是在计算大小,并没有实际拿到内存;现在有的是一根16个header的链表,每一个header管理1M内存,并且还有两根指针,一根指向真正的内存,另一根指向region(管理中心)
-
__sbh_alloc_new_region():占16K
每个group里有64对指针,形成双向链表(1M被分成32块,1块32k,每块由一个group控制),group使用的内存由前面的4096提供,此时的1M是向操作系统要的保留的(并未真正分配了内存空间—虚拟地址空间)
-
__sbh_alloc_new_group()
每一个group又被分成8页,每页4k
group的第一条链表负责16字节,第二条32,…最后一条不是1024,而是大于1024的全归它,此时被挂载在最后一对,之后若切小了可能会挂载在前面
整理一下:
左边fdfdfd的作用(无人区):当用户拿到这块内存的时候,可能会有意无意的写的超过该块的大小,那么下面那个fdfdfd就被破坏了,编译器在debug模式下就会检测到并发出警告
第二次内存分配:假设此时又要分配240字节,它会先去找对应的链表是否有内存被挂载,此时没找到,便向后继续找,在最后一对链表中找到了,所以继续在这里切
释放动作:
此时要释放的内存为240字节,把cookie就表示已经回收了(可能会把里面的内存涂抹),经计算需要由35号链表来回收,并要将对应的byte由0改成1
继续分配,向后找找到刚刚的第35号分配,剩下的小内存块计算后挂载到24号链表,并调整里面的byte
回收动作
此时cookie设为0回收灰色这块必须要判断上下内存是否已经被回收,若已经被回收需要合并,通过上下cookie可以完成这个操作,下cookie才能保证往上合并,合并完成后挂载到对应链表
如何判断全回收:通过刚刚的计数器,为0时代表可以全部归还给操作系统了
不要急躁:为了避免刚刚归还又要继续分配的“抖动”,只有当下一块被标记为可回收时,这块才会被回收
第四讲 loki::allocator
利用三个class,每个里面各有两根指针指向下一级
第五讲 other allocators
new_allocator
内部的allocate和deallocate分别调用了::operator new和operator delete,没有实际进行内存管理动作
malloc_allocator
内部的allocate和deallocate分别调用了malloc和free,我们不能重载其动作
array_allocator
内部为一根指针和一个数组,由指针来操作数组。因为它的内部是数组,所以需要提前知道大小,并且内部对deallocate函数无任何操作
debug_allocator
内部为一个分配器和一个unsign_int(相当于封装了别的分配器,并多用一块空间来记录整块的大小,没什么屌用)
__pool_allocator
在前面已经讨论过,只拿不还
bitmap_allocator
blocks:容器要的元素(如果是链表,那它的两根链表也是算在block里的),block的大小就是那个元素的大小,这里以8字节的double为例。第一次默认分配64个block,用完了会成倍增长,元素大小相同不会被存在一起,这里是以类型区分的
super-blocks:= blocks + bitmap + use count(有几块被用了)
bitmap:这里用两个32位的整数凑成64位,每一位表示对应位置的分配情况,1表示未被分配,0表示已分配,这里全F表示全未被分配
mini-vector:内部自己写了一个和vector一样的容器来操作block
第一次分配:
假设分配了63块:
回收了一块:
第一个super-blocks用尽:
第二个super-blocks用尽:
每次全回收会使下一次的分配规模减半
第一个super-blocks全回收:
使用令一个mini-vector来指向要全回收的super-blocks代表已经全回收了,当数量超过64,便会把最大的归还给操作系统
第二个super-blocks全回收:
如果接下来要重新分配的大小相同,则会先从回收的那个mini-vector中将内存挂回