c++内存管理(详解allocator、malloc原理)

整理于侯捷的c++内存管理,侵删

第一讲 primitives

直接挖一大块,然后切分成一小块一小块的,里面放置要存放的对象(减少了调用malloc的次数)

概述

当c++应用程序想获取一块内存时:
可以通过以下方式来获取内存

分配释放类型可否重载
malloc()free()C函数不可
newdeleteC++表达式不可
::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

image-20200619131249644

在array new之后,内存中开辟额外空间存放cookie,来记录创建了多少个对象,在调用delete时会根据cookie来free掉这里面的东西,由于这个对象内部成员变量没有指针,对象的析构函数并没有作用,所以在这种情况下,使用delete和delete[ ]没有区别。

image-20200619131930841

在array new包含指针的对象后,使用delete将只唤起一次dtor,指针由于有cookie可以全部释放,但是指针指向的对象只调用了一次析构函数,只释放了一个,造成内存泄漏,所以,在使用array new的时候都建议使用array delete来释放

总结:

  • 对class without ptr member 可能没有影响
  • 对class with pointer member 通常有影响

两张重要的截图:

image-20200619134740551

image-20200619134618991

若创建的是对象,并且对象的析构函数有意义,那么它就会多一块位置来存放它创建的个数,若使用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++应用程序,分配内存的途径:

image-20200619141533635

c++容器,分配内存的途径:

image-20200619141844416

重载::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只有两个选择

  1. 让更多的memory可用
  2. 调用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运作模式分析

image-20200826213720155

  • 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)

检讨

  1. alloc手中还有多少资源?以上白色皆是。可不可以将白色的小区块合成为较大区块供应给客户?技术难度极高

  2. system heap中还剩多少资源?10000-9688=312.可不可以将失败的那次索取折半…再折半…最终索取量<=312便能获得满足

第三讲 malloc/free

运作模式分析

image-20200628171147724

只有在申请的内存小于1016字节(1024-8(cookie))使用sbh分配内存,大于则直接由操作系统分配,接下来讨论的是由sbh的分配过程

_heap_init()

image-20200628172200438

先调用操作系统函数获取一块内存,大小为4096(无实际意义,后面不够了操作系统会把它增长)

_sbh_heap_init()

从先前分配好的内存中取16个header大小的内存,并用指针指向这16个header(每一个header管理1M内存,这1M只是虚拟地址空间)

header的结构:

image-20200628173251846

_ioinit()

程序的第一次内存分配(256字节)

第一步先判断当前是否在debug模式,如果是则进入分配debug内存的函数,我们讨论debug

_heap_alloc_dbg()

在要分配的内存头尾填上4*8+4,便于debug(当前只是在调整大小,并未正式分配)

image-20200628225646597

_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():占16Kimage-20200628220612600

    每个group里有64对指针,形成双向链表(1M被分成32块,1块32k,每块由一个group控制),group使用的内存由前面的4096提供,此时的1M是向操作系统要的保留的(并未真正分配了内存空间—虚拟地址空间)

  • __sbh_alloc_new_group()image-20200628221753120

    每一个group又被分成8页,每页4k

    image-20200628222912698

    group的第一条链表负责16字节,第二条32,…最后一条不是1024,而是大于1024的全归它,此时被挂载在最后一对,之后若切小了可能会挂载在前面

    image-20200628224846985

    整理一下:

    image-20200628231938389

    左边fdfdfd的作用(无人区):当用户拿到这块内存的时候,可能会有意无意的写的超过该块的大小,那么下面那个fdfdfd就被破坏了,编译器在debug模式下就会检测到并发出警告

    第二次内存分配:假设此时又要分配240字节,它会先去找对应的链表是否有内存被挂载,此时没找到,便向后继续找,在最后一对链表中找到了,所以继续在这里切

    释放动作:

    image-20200628232136585

    此时要释放的内存为240字节,把cookie就表示已经回收了(可能会把里面的内存涂抹),经计算需要由35号链表来回收,并要将对应的byte由0改成1

    image-20200628232657322

    继续分配,向后找找到刚刚的第35号分配,剩下的小内存块计算后挂载到24号链表,并调整里面的byte

    回收动作

    image-20200628232927545

    此时cookie设为0回收灰色这块必须要判断上下内存是否已经被回收,若已经被回收需要合并,通过上下cookie可以完成这个操作,下cookie才能保证往上合并,合并完成后挂载到对应链表

    image-20200628233545923

    如何判断全回收:通过刚刚的计数器,为0时代表可以全部归还给操作系统了

    不要急躁:为了避免刚刚归还又要继续分配的“抖动”,只有当下一块被标记为可回收时,这块才会被回收

第四讲 loki::allocator

image-20200630213440616

利用三个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

image-20200701164900195

blocks:容器要的元素(如果是链表,那它的两根链表也是算在block里的),block的大小就是那个元素的大小,这里以8字节的double为例。第一次默认分配64个block,用完了会成倍增长,元素大小相同不会被存在一起,这里是以类型区分的

super-blocks:= blocks + bitmap + use count(有几块被用了)

bitmap:这里用两个32位的整数凑成64位,每一位表示对应位置的分配情况,1表示未被分配,0表示已分配,这里全F表示全未被分配

mini-vector:内部自己写了一个和vector一样的容器来操作block

第一次分配

image-20200701170041787

假设分配了63块

image-20200701170125990

回收了一块

image-20200701170557807

第一个super-blocks用尽

image-20200701170333872

第二个super-blocks用尽

image-20200701170758780

每次全回收会使下一次的分配规模减半

第一个super-blocks全回收

image-20200701171557234

使用令一个mini-vector来指向要全回收的super-blocks代表已经全回收了,当数量超过64,便会把最大的归还给操作系统

第二个super-blocks全回收

如果接下来要重新分配的大小相同,则会先从回收的那个mini-vector中将内存挂回

补充:

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值