空间配置器allocator

空间配置器的作用是在底层为上层的各种容器提供存储空间,需要多少分配多少,一般分配的比你需要的更多。打个不恰当的比喻,空间配置器和容器之间的关系,相当于将军和粮草的关系。当然了,容器相当于将军,它在阵前杀敌,冲锋陷阵,处理各种事情;而空间配置器就相当于粮草,给前阵提供源源不断的供给;如果一个将军想打胜仗,那必须后方粮草充足才行。

为了进一步提高内存的使用率和使用效率。主要是从以下两方面来考虑的:

1.小块内存会带来内存碎片问题

如果任由STL中的容器自行通过malloc分配内存,那么频繁的分配和释放内存会导致堆中有很多的外部碎片。可能堆中的所有空闲空间之和很大,但当申请新的内存的请求到来时,没有足够大的连续内存可以分配,这将导致内存分配失败。因此这样会导致内存浪费。

2.小块内存的频繁申请释放会带来性能问题

开辟空间的时候,分配器需要时间去寻找空闲块,找到空闲块之后才能分配给用户。而如果分配器找不到足够大的空闲块可能还需要考虑处理加碎片现象(释放的小块空间没有合并),这时候需要花时间去合并已经释放了的内存空间块。

而且malloc在开辟内存空间的时候,还会附带附加的额外信息,因为系统需要靠多出来的额外信息管理内存。特别是区块越小,额外负担所占的比例就越大,更加显得浪费。

考虑到小型区块可能造成的内存碎片问题,SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128字节时,视之为“足够大”,便调用第一级配置器;当配置区块小于128字节时,视之为“过小",为了降低额外负担,便采用复杂的memory pool整理方式,将会调用二级空间配置器直接去内存池中申请,而不再求助于第一级配置器。整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC是否被定义。

空间配置器文件组成

  • 定义了全局函数construct()和destroy(),负责对象的构造和析构。
  • 定义了一二级配置器,配置器统称为alloc而非allocator!
  • 定义了一些全局函数,用来填充(fill)或者复制(copy)大块内存数据,也隶属于STL标准规范。

一级空间配置器设计与实现 

一级空间配置器只是简单的封装了一下malloc和free实现的。在allocate函数中如果通过malloc申请内存失败(失败返回0)就改用oom_malloc(size_t n)函数尝试分配内存,如果oom发现没有指定new-handler函数的话,那就直接调__THROW_BAD_ALLOC,丢出bad_alloc或是直接通过exit(1)中止程序。

我们来看看allocate函数,这个函数执行的流程就如上面所诉:

static void * allocate(size_t n)
{
    void *result = malloc(n);//直接使用第一级分配器,直接使用malloc
    if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory)
    return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型
}

再来看看oom_malloc函数是如何实现的,很显然这里的内存不足处理函数是需要客户端来实现的,默认情况下为0,也即默认情况下将会直接执行__THROW_BAD_ALLOC。内存不足处理例程是客端的责任,设定内存不足处理例程也是客端的责任。如果你已经设置了out-of-memory handler,那么系统会调用你设定的处理程序,企图释放内存来给程序用。会一直循环直到成功分配到内存才结束。所以这个函数设计不好的话会出现死循环!这也是为什么STL里面默认没有设置处理机制。

// malloc_alloc out-of-memory handling
// 初值为0.有待客端设定
template 
void (* __malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;
 
template 
void * __malloc_alloc_template::oom_malloc(size_t n)
{
    void (* my_malloc_handler)();
    void *result;
 
    for (;;) {//这个循环将不断尝试释放、配置、再释放、再配置
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*my_malloc_handler)();//这个函数将试图释放内存
        result = malloc(n);//分配内存
        if (result) return(result);//如果内存已经得到满足的话,那么就可以返回了,如果不满足,那么
        //就继续释放,分配....
    }
}

过程如下图:

空间配置器allocator_第1张图片

接下来我们看一个简单的vector的实现。

template
class Vector
{
public:
	// 构造函数
	Vector(int size = 0)
		:mcur(0), msize(size)
	{
		mpvec = new T[msize];
	}
	// 析构函数
	~Vector()
	{
		delete[]mpvec;
		mpvec = nullptr;
	}
	// 拷贝构造函数
	Vector(const Vector &src)
		:mcur(src.mcur), msize(src.msize)
	{
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
	}
	// 赋值重载函数
	Vector& operator=(const Vector &src)
	{
		if (this == &src)
			return *this;

		delete []mpvec;

		mcur = src.mcur;
		msize = src.msize;
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
		return *this;
	}
	// 尾部插入数据函数
	void push_back(const T &val)
	{
		if (mcur == msize)
			resize();
		mpvec[mcur++] = val;
	}
	// 尾部删除数据函数
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
	}
private:
	T *mpvec; // 动态数组,保存容器的元素
	int mcur; // 保存当前有效元素的个数
	int msize; // 保存容器扩容后的总长度

	// 容器2倍扩容函数
	void resize()
	{
	    /*默认构造的vector对象,内存扩容是从0-1-2-4-8-16-32-...的2倍方式
		进行扩容的,因此vector容器的初始内存使用效率特别低,可以使用reserve
		预留空间函数提供容器的使用效率。*/
		if (msize == 0)
		{
			mpvec = new T[1];
			mcur = 0;
			msize = 1;
		}
		else
		{
			T *ptmp = new T[2 * msize];
			for (int i = 0; i < msize; ++i)
			{
				ptmp[i] = mpvec[i];
			}
			delete[]mpvec;
			mpvec = ptmp;
			msize *= 2;
		}
	}
};
// 一个简单的测试类A
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
};
int main()
{
	Vector vec(10); // 10表示底层开辟的空间大小,但是却构造了10个A对象
	A a1, a2, a3;
	cout << "---------------" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "---------------" << endl;
	vec.pop_back(); // 删除a3没有对对象进行析构
	cout << "---------------" << endl;

	// vec容器析构时,内部只有2个有效的A对象,但是却析构了10次
	return 0;
}

运行上面的代码,打印结果如下:
A()
A()
A()
A()
A()
A()
A()
A()
A()
A() // 上面到这个是vector容器中,构造了10个对象
A() // 这里开始下面的三个A构造函数,是构造了a1, a2, a3三个对象
A()
A()
+++++++++++++++
+++++++++++++++
+++++++++++++++ // 这里有问题,vec.pop_back()删除末尾A对象,但是并没有进行析构调用,有可能造成资源泄露
~A()
~A()
~A() // 上面到这里的三个析构函数,析构了a1, a2, a3三个对象
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A() // 上面到这里的10个析构函数,是把vector容器中的对象全部进行析构

上面的代码有这样三个问题如下:

  1. 定义容器时Vector< A > vec(10),我们希望底层开辟可以容纳10个元素的空间,并不需要给我构造10个A对象,因为此时我还没有打算给容器添加数据,这么多构造函数的调用,纯粹是效率的浪费。
  2. 从容器中删除元素时vec.pop_back(),这句代码的意思是删除了容器末尾的对象A,但是并没有调用A对象的析构函数,如果A对象占用了外部资源,那么资源的释放代码肯定在A的析构函数里面,这样就造成了资源泄露的问题。
  3. vec容器在出函数作用域析构的时候,并没有析构有效的A对象,其实上面代码中,最终vec容器只有两个我们放入的有效对象a1和a2,a3被删除了,应该只析构两次就可以,但是却析构了10次,不合理。

对于这三个问题我们的解决方式是这样的:

  1. 当定义一个容器的时候,应该只申请内存,不用构造那么多无效对象,当给容器添加元素的时候,在相应的位置再构造对象就可以了。
  2. 从容器中删除元素的时候,不应该只做- -mcur,还应该调用删除对象的析构函数,释放对象占用的外部资源。
  3. 当容器vec析构的时候,应该把容器中有效的对象进行析构,然后再释放整个容器的内存资源。

基于以上的问题解决方式,我们有这样的一个需求:

  • 把对象的内存开辟和构造函数调用两个过程分开
  • 把对象析构和内存释放两个过程分开

所以,此时new和delete就不能在容器中直接使用了,因为new不仅可以开辟内存,还自动调用构造函数构造对象;delete要先析构对象,然后紧接着释放内存。那么,上面这个需求任务由谁来完成呢?就是今天要介绍的这个容器的空间配置器allocator。

空间配置器allocator_第2张图片

下面提供一个类似C++ STL库里面的空间配置器代码的实现

// 自定义空间配置器
template
struct myallocator
{
	// 开辟内存空间
	T* allocate(size_t size) 
	{
		return (T*)::operator new(sizeof(T)*size);// 相当于malloc分配内存
	}
	// 释放内存空间
	void deallocate(void *ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T)*size);// 相当于free释放内存
	}
	// 负责对象构造
	void construct(T *ptr, const T &val)
	{
		new ((void*)ptr) T(val);// 用定位new在指定内存上构造对象
	}
	// 负责对象析构
	void destroy(T *ptr)
	{
		ptr->~T();// 显示调用对象的析构函数
	}
};

上面实现的空间配置器比较简单,内存管理依然用的是operator new和operator delete,其实就是malloc和free的内存管理,当然我们还可以给allocator附加一个内存池的实现,相当于是自定义内存管理方式。

可以看看C++ STL库中vector容器的类模板定义头,代码如下:
 

template>
	class vector

从上面的vector容器类模板定义处可以看到,它有两个模板类型参数,_Ty是容器存放的数据的类型,_Alloc就是空间配置器的类型,如果用户没有自定义,会使用库里面默认的allocator,类似于我们上面提供的空间配置器代码的实现。

把我们最开始提供的vector代码,添加上我们自己实现的空间配置器allocator,修改代码如下:


// 自定义空间配置器
template
struct myallocator
{
	// 开辟内存空间
	T* allocate(size_t size) 
	{
		return (T*)::operator new(sizeof(T)*size);// 相当于malloc分配内存
	}
	// 释放内存空间
	void deallocate(void *ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T)*size);// 相当于free释放内存
	}
	// 负责对象构造
	void construct(T *ptr, const T &val)
	{
		new ((void*)ptr) T(val);// 用定位new在指定内存上构造对象
	}
	// 负责对象析构
	void destroy(T *ptr)
	{
		ptr->~T();// 显示调用对象的析构函数
	}
};

/*
给Vector容器的实现添加空间配置器allocator
*/
template>
class Vector
{
public:
	// 构造函数,可以传入自定以的空间配置器,否则用默认的allocator
	Vector(int size = 0, const allocator &alloc = allocator())
		:mcur(0), msize(size), mallocator(alloc)
	{
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
	}
	// 析构函数
	~Vector()
	{
		// 先析构容器中的对象
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec+i);
		}
		// 释放容器占用的堆内存
		mallocator.deallocate(mpvec, msize);
		mpvec = nullptr;
	}
	// 拷贝构造函数
	Vector(const Vector &src)
		:mcur(src.mcur)
		, msize(src.msize)
		, mallocator(src.mallocator)
	{
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			// 在指定的地址mpvec+i上构造一个值为src.mpvec[i]的对象
			mallocator.construct(mpvec+i, src.mpvec[i]);
		}
	}
	// 赋值重载函数
	Vector operator=(const Vector &src)
	{
		if (this == &src)
			return *this;

		// 先析构容器中的对象
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec + i);
		}
		// 释放容器占用的堆内存
		mallocator.deallocate(mpvec, msize);

		mcur = src.mcur;
		msize = src.msize;
		// 只开辟容器底层空间,不构造任何对象
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			// 在指定的地址mpvec+i上构造一个值为src.mpvec[i]的对象
			mallocator.construct(mpvec + i, src.mpvec[i]);
		}
		return *this;
	}
	// 尾部插入数据函数
	void push_back(const T &val)
	{
		if (mcur == msize)
			resize();
		mallocator.construct(mpvec + mcur, val);
		mcur++;
	}
	// 尾部删除数据函数
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
		// 析构被删除的对象
		mallocator.destroy(mpvec + mcur);
	}
private:
	T *mpvec; // 动态数组,保存容器的元素
	int mcur; // 保存当前有效元素的个数
	int msize; // 保存容器扩容后的总长度
	allocator mallocator; // 定义容器的空间配置器对象

	// 容器2倍扩容函数
	void resize()
	{
		if (msize == 0)
		{
			mpvec = mallocator.allocate(sizeof(T));
			mcur = 0;
			msize = 1;
		}
		else
		{
			T *ptmp = mallocator.allocate(2 * msize);
			for (int i = 0; i < msize; ++i)
			{
				mallocator.construct(ptmp + i, mpvec[i]);
			}
			// 先析构容器中的对象
			for (int i = 0; i < msize; ++i)
			{
				mallocator.destroy(mpvec + i);
			}
			// 释放容器占用的堆内存
			mallocator.deallocate(mpvec, msize);
			mpvec = ptmp;
			msize *= 2;
		}
	}
};
// 一个简单的测试类A
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
};
int main()
{
	Vector vec(10); // 此处只开辟内存,没有构造任何对象
	A a1, a2, a3;
	cout << "+++++++++++++++" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "+++++++++++++++" << endl;
	vec.pop_back(); // 删除a3并析构a3对象
	cout << "+++++++++++++++" << endl;

	// vec容器析构时,内部只有2个有效的A对象,析构了2次,正确
	return 0;
}

代码运行打印如下:
A()
A()
A() // 上面三个是A a1, a2, a3;三个栈上对象的打印
+++++++++++++++
+++++++++++++++
~A() // 此处是vec.pop_back()析构了a3对象
+++++++++++++++
~A()
~A()
~A() // 上面三个是A a1, a2, a3;三个对象的析构调用
~A()
~A() // 上面两个是vec容器中两个A对象的析构

通过打印可以看到,最开始实现的容器,我们提到的这三个问题:

  • 定义容器时Vector< A > vec(10),我们希望底层开辟可以容纳10个元素的空间,并不需要给我构造10个A对象,因为此时我还没有打算给容器添加数据,这么多构造函数的调用,纯粹是效率的浪费。
  • 从容器中删除元素时vec.pop_back(),这句代码的意思是删除了容器末尾的对象A,但是并没有调用A对象的析构函数,如果A对象占用了外部资源,那么资源的释放代码肯定在A的析构函数里面,这样就造成了资源泄露的问题。
  • vec容器在出函数作用域析构的时候,并没有析构有效的A对象,其实上面代码中,最终vec容器只有两个我们放入的有效对象a1和a2,a3被删除了,应该只析构两次就可以,但是却析构了10次,不合理。

现在都通过空间配置器allocator解决了,仔细对比最开始的Vector和修改后带空间配置器版本的Vector的代码实现,体会allocator在容器中的具体使用。

二级空间配置器

二级空间配置器使用内存池+自由链表的形式避免了小块内存带来的碎片化,提高了分配的效率,提高了利用率。SGI的做法是先判断要开辟的大小是不是大于128,如果大于128则就认为是一块大块内存,调用一级空间配置器直接分配。否则的话就通过内存池来分配,假设要分配8个字节大小的空间,那么他就会去内存池中分配多个8个字节大小的内存块,将多余的挂在自由链表上,下一次再需要8个字节时就去自由链表上取就可以了,如果回收这8个字节的话,直接将它挂在自由链表上就可以了。
 为了便于管理,二级空间配置器在分配的时候都是以8的倍数对齐。也就是说二级配置器会将任何小块内存的需求上调到8的倍数处(例如:要7个字节,会给你分配8个字节。要9个字节,会给你16个字节),尽管这样做有内碎片的问题,但是对于我们管理来说却简单了不少。因为这样的话只要维护16个free_list就可以了,free_list这16个结点分别管理大小为8,16,24,32,40,48,56,64,72,80,88,86,96,104,112,120,128字节大小的内存块就行了。

自由链表的结点类型:

union _Obj
{
       union _Obj* _M_free_list_link;
       char _M_client_data[1];    /* The client sees this.        */
};

空间配置器allocator_第3张图片

空间配置器allocator_第4张图片

为了将自由链表下面的结点串起来,又不引入额外的指针,所以我们要学会一物两用,因为在自由链表下面挂的最小的内存块都是8个字节,足够存放一个地址,所以我们就在这些内存块里面存放其他内存块的地址,这样的话就将这些内存块链接起来了。

空间配置器allocator_第5张图片

enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作
enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128
enum { _NFREELISTS = 16 };       //自由链表的长度,等于_MAXBYTES/_ALIGN
template   //非模板类型参数
class _DefaultAllocTemplate
{
       union _Obj                      //自由链表结点的类型
       {
              _Obj* _freeListLink;         //指向自由链表结点的指针
              char _clientData[1];          //this client sees
       };
private:
       static char* _startFree;             //内存池的头指针
       static char* _endFree;               //内存池的尾指针
       static size_t _heapSize;              //记录内存池已经向系统申请了多大的内存
       static _Obj* volatile _freeList[_NFREELISTS];    //自由链表
private:
       static size_t _GetFreeListIndex(size_t bytes)   //得到这个字节对应在自由链表中应取的位置
       {
              return (bytes +(size_t) _ALIGN - 1) / (size_t)_ALIGN - 1;     
       }
       static size_t _GetRoundUp(size_t bytes)        //对这个字节向上取成8的倍数
       {
              return (bytes + (size_t)_ALIGN - 1)&(~(_ALIGN-1));     //将n向上取成8的倍数
       }
       static void* _Refill(size_t n);          //在自由链表中申请内存,n表示要的内存的大小
       static char* _chunkAlloc(size_t size,int& nobjs);    //在内存池中申请内存nobjs个对象,每个对象size个大小
public:
       static void* Allocate(size_t n);      //n要大于0
       static void DeAllocate(void *p,size_t n);        //n要不等于0
};

二级空间配置器的逻辑步骤:
假如现在申请n个字节:
1、判断n是否大于128,如果大于128则直接调用一级空间配置器。如果不大于,则将n上调至8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面挂有未使用的内存,则摘下来直接返回这块空间的地址。否则的话我们就要调用refill(size_t n)函数去内存池中申请。

2、向内存池申请的时候可以多申请几个,STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。
  进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。
  这时候就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。
  如果chunk_alloc失败的话,在他内部有处理机制。

3、进入chunk_alloc(size_t n,size_t& nobjs )向内存池申请空间的话有三种情况:
  3.1、内存池剩余的空间足够nobjs*n这么大的空间,则直接分配好返回就可以了。
  3.2、内存池剩余的空间leftAlloc的范围是n<=leftAlloc   3.3、内存池中剩余的空间连一个n都不够了,这时候就要向heap申请内存,不过在申请之前先要将内存池中剩余的内存挂到自由链表上,之后再向heap申请。
   3.3.1、如果申请成功的话,则就再调一次chunk_alloc重新分配。
   3.3.2、如果不成功的话,这时候再去自由链表中看看有没有比n大的空间,如果有就将这块空间还给内存池,然后再调一次chunk_alloc重新分配。
   3.3.3、如果没有的话,则就调用一级空间配置器分配,看看内存不足处理机制能否处理。

enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作
enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128
enum { _NFREELISTS = 16 };       //自由链表的长度,等于_MAXBYTES/_ALIGN
template   //非模板类型参数
class _DefaultAllocTemplate
{
       union _Obj                      //自由链表结点的类型
       {
              _Obj* _freeListLink;         //指向自由链表结点的指针
              char _clientData[1];          //this client sees
       };
private:
       static char* _startFree;             //内存池的头指针
       static char* _endFree;               //内存池的尾指针
       static size_t _heapSize;              //记录内存池已经向系统申请了多大的内存
       static _Obj* volatile _freeList[_NFREELISTS];    //自由链表
private:
       static size_t _GetFreeListIndex(size_t bytes)   //得到这个字节对应在自由链表中应取的位置
       {
              return (bytes +(size_t) _ALIGN - 1) / (size_t)_ALIGN - 1;     
       }
       static size_t _GetRoundUp(size_t bytes)        //对这个字节向上取成8的倍数
       {
              return (bytes + (size_t)_ALIGN - 1)&(~(_ALIGN-1));     //将n向上取成8的倍数
       }
       static void* _Refill(size_t n);          //在自由链表中申请内存,n表示要的内存的大小
       static char* _chunkAlloc(size_t size,int& nobjs);    //在内存池中申请内存nobjs个对象,每个对象size个大小
public:
       static void* Allocate(size_t n);      //n要大于0
       static void DeAllocate(void *p,size_t n);        //n要不等于0
};
template
char* _DefaultAllocTemplate::_startFree = 0;        //内存池的头指针
template
char* _DefaultAllocTemplate::_endFree=0;           //内存池的尾指针
template
size_t _DefaultAllocTemplate::_heapSize = 0;              //记录内存池已经向系统申请了多大的内存
template
typename _DefaultAllocTemplate::_Obj* volatile      //前面加typename表示后面是个类型
_DefaultAllocTemplate::_freeList[_NFREELISTS] = {0};    //自由链表
 
template
void* _DefaultAllocTemplate::Allocate(size_t n)    //分配空间
{
       void *ret;
       //先判断要分配的空间大小是不是大于128个字节
       if (n>_MAXBYTES)      //大于_MAXBYTES个字节则认为是大块内存,直接调用一级空间配置器
       {
              ret = malloc_alloc::_Allocate(n);
       }
       else       //否则就去自由链表中找
       {
              _Obj* volatile *myFreeList = _freeList+_GetFreeListIndex(n);  //让myFreeList指向自由链表中n向上取8的整数倍
              _Obj* result = *myFreeList;
              if (result == 0)  //这个结点下面没有挂内存,则就要去内存池中申请
              {
                     ret = _Refill(_GetRoundUp(n));      //到内存池中申请
              }
              else            //已经在自由链表上找到了内存
              {
                     *myFreeList= result->_freeListLink;      //把第二块空间的地址放到自由链表上
                     ret = result;
              }
       }
       return ret;
}
template
void _DefaultAllocTemplate::DeAllocate(void *p, size_t n)   //回收空间
{
       //先判断这个字节的大小
       if (n > _MAXBYTES)  //如果n大于自由链表中结点所能挂的最大内存块,则就直接调用一级指针的释放函数
       {
              malloc_alloc::_DeAllocate(p);
       }
       else        //将这块内存回收到自由链表中
       {
              _Obj* q = (_Obj*)p;
              _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n);
              q->_freeListLink = *myFreeList;
              *myFreeList = q;
       }
}
 
template
void* _DefaultAllocTemplate::_Refill(size_t n)     //n表示要申请的字节个数
{
       int nobjs = 20;           //向内存池申请的时候一次性申请20个
       char* chunk = _chunkAlloc(n,nobjs);    //因为现在链表中没有,所以要想内存池中申请,多余的再挂到自由链表下面
       if (1 == nobjs)          //只分配到了一个对象
       {
              return chunk;
       }
       _Obj* ret = (_Obj*)chunk;                  //将申请的第一个对象作为返回值
       _Obj* volatile *myFreeList = _freeList+ _GetFreeListIndex(n);
       *myFreeList =(_Obj*)(chunk+n);             //将第二个对象的地址放到自由链表中
       _Obj* cur= *myFreeList;
       _Obj* next=0;
       cur->_freeListLink = 0;
       for (int i = 2; i < nobjs; ++i)             //将剩下的块挂到自由链表上
       {
              next= (_Obj*)(chunk + n*i);
              cur->_freeListLink = next;
              cur = next;
       }
       cur->_freeListLink = 0;
       return ret;
}
template
char* _DefaultAllocTemplate::_chunkAlloc(size_t size, int& nobjs)  //向系统中申请内存
{
       char* result = 0;
       size_t totalBytes = size*nobjs;        //总共请求的内存大小
       size_t leftBytes = _endFree - _startFree;      //内存池剩余的大小
       if (leftBytes>=totalBytes)     //如果剩余的大小大于等于申请的大小,则返回这个这内存
       {
              result = _startFree;
              _startFree += totalBytes;
              return result;
       }
       else if (leftBytes>size)         //如果剩余的内存足够分配一个size,
       {
              nobjs=(int)(leftBytes/size);
              result = _startFree;
              _startFree +=(nobjs*size);
              return result;
       }
       else            //内存池中的内存已经不够一个size了
       {
              size_t NewBytes = 2 * totalBytes+_GetRoundUp(_heapSize>>4);       //内存池要开辟的新的容量
              if (leftBytes >0)  //剩余的内存挂到自由链表上
              {
                     _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(leftBytes);
                     ((_Obj*)_startFree)->_freeListLink = *myFreeList;
                     *myFreeList = (_Obj*)_startFree;
              }
              
              //开辟新的内存
              _startFree = (char*)malloc(NewBytes);
              if (0 == _startFree)                   //如果开辟失败
              {
                     //如果开辟失败的话,则表明系统已经没有内存了,这时候我们就要到自由链表中找一块比n还大的内存块,如果还没有的话,那就掉一级空间配置器
                     for (size_t i = size; i <(size_t)_MAXBYTES;i+=(size_t)_ALIGN)
                     {
                           _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(i);
                           _Obj* p =*myFreeList;
                           if (NULL != p)       //在自由链表找到一块内存块
                           {
                                  _startFree =(char*)p;                  
                                  //将这个内存块摘下来给内存池
                                  *myFreeList = p->_freeListLink;
                                  _endFree = _startFree + i;
                                  return _chunkAlloc(size, nobjs);  //内存池开辟好的话,就再调一次chunk分配内存
                           }
                     }
                     //要是再找不到的话,就调一级空间配置器,其中有内存不足处理机制,要是还不行的话,他会自动抛出异常
                     _endFree = NULL;
                     _startFree=(char*)malloc_alloc::_Allocate(NewBytes);
              }      
              //开辟成功的,就更新heapSize(记录总共向系统申请了多少内存),,更新_endFree
              _heapSize += NewBytes;
              _endFree = _startFree + NewBytes;
              return _chunkAlloc(size, nobjs);             //内存池开辟好的话,就再调一次chunk分配内存
       }
}
 
typedef _DefaultAllocTemplate<0,0>  default_alloc;

空间配置器的其他问题:
1、在空间配置器中所有的函数和变量都是静态的,所以他们在程序结束的时候才会被释放发。二级空间配置器中没有将申请的内存还给操作系统,只是将他们挂在自由链表上。所以说只有当你的程序结束了之后才会将开辟的内存还给操作系统。

2、由于它没有将内存还给操作系统,所以就会出现二种极端的情况。
 2.1、假如我不断的开辟小块内存,最后将整个heap上的内存都挂在了自由链表上,但是都没有用这些空间,再想要开辟一个大块内存的话会开辟失败。
 2.2、再比如我不断的开辟char,最后将整个heap内存全挂在了自由链表的第一个结点的后面,这时候我再想开辟一个16个字节的内存,也会失败。
  或许我比较杞人忧天吧,总的来说上面的情况只是小概率情况。如果非得想办法解决的话,我想的是:针对2.1我们可以引入释放二级空间配置器的方法,但是这个释放比较麻烦。针对2.2我们可以合并自由链表上的连续的小的内存块。

3、二级空间配置器会造成内碎片问题,极端的情况下一直申请char,则就会浪费7/8的空间。但是整体来说,空间配置器的性能还是蛮高的。

总流程:

 

 

你可能感兴趣的:(C++)