STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)

一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的

研究了好久才写好的,主要是二级配置器,大标题小标题什么的可能没有安排好,先
写了原理上的内容,再剖析了各个函数源码,各个目录可以看csdn自带的目录(


如何使用空间配置器

每个容器都会通过默认参数指定好allocator,不需要显示声明,如定义一个vector类容器:std::vector vecTemp;而完整的vecTemp声明应该是vector> vecTemp;

加入自定义了将内存分配指向磁盘或者其他存储介质空间的allocator,那么只要在声明时传入设计好的allocator,不再使用默认的allocator就行了

那么,如何设计一个allocator呢?

SGI STL空间配置器架构

一个空间配置器最基本的功能有四:申请内存、构造对象、析构对象、释放内存;
但是我们想要设计一个在STL中用的空间配置器的话,光有上述四个是不行的,因为STL对allocator的组成已经规定好了,即有STL规范

由于一个内存配置与释放通常分两个阶段,而STL allocator 将这两个阶段的操作区分开来:

  1. 内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责
  2. 对象构造由::construct()负责,对象析构由::destroy()负责
    其实,对于内存配置和释放还有allocator::allocate()allocator::deallocate(), 但它就是对::operator new::operator delete做了一层薄薄的封装, 效率不好就不要使用了

STL标准规格告诉我们,配置器定义于中, SGI 包含以下两个文件:
1, #include//负责内存空间的配置与释放
2, #include//负责对象内容的构造与析构

STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)_第1张图片

构造和析构的基本工具:construct()和destroy()

书P52~53, 理解是简单的
STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)_第2张图片construct()就是接受一个指针p和一个初值value,用途就是将初值设定到指针所致的空间上,可以通过placement new来完成。

destroy()有两个版本:第一个版本:接受一个指针,直接调用析构函数即可;第二个版本:接受一个迭代器区间,将区间范围内的对象析构掉

空间的配置与释放,alloc

考虑到小型区块所可能造成的内存破损问题,SGI设计了双层级配置器
分界点是配置的内存是否大于128B,大于就用第一级配置器直接使用malloc()和free(),小于等于则通过第二级访问复杂的memory pool整理方式。

通过是否定义_USE_MALLOC宏,来设定是只打开第一级还是同时打开第一级与第二级。SGI STL没定义那个宏,也就是同时开放一、二级。

第一级配置器_malloc_alloc_template

一级空间配置器就是更大程度来合理运用空间,它的内部设计实际就是为了压榨剩余的内存,达到内存的高效运用, 一级空间配置器内部其实就是mallocfree的封装,然后尽量开辟想要的内存空间,==就算系统内部的剩余内存空间小于你所申请的内存空间,==它都会努力尝试开辟出来


一级空间配置器的声明:

/*__malloc_alloc_template*/
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;

一级配置器的重要函数有:allocate(开辟空间)reallocate(开辟空间)deallocate(释放空间)

①首先看看allocate及相关函数的实现

第一配置器直接调用malloc(),当malloc分配失败时,改用oom_malloc(), oom = out of memory, 处理内存不足的函数
关于oom_malloc函数:它的初值是定义为0,即必须用户自定义相应的内存不足处理函数才能执行,否则函数指针=0还是会抛出异常;
这个函数叫内存不足处理函数,在代码中会定义对应的函数指针,该指针所指向的函数必须由用户定义,因为只有用户知道哪些内存可以被释放来腾出空间,如果没有为该函数指针赋予相应的函数,则此时直接会抛出bad_alloc异常,若该函数指针被指定,则会不停调用该函数,直到申请到足够的内存,这里把它叫做内存不足处理函数

体现在代码中,oom函数是在一个无限循环中,退出循环的一个条件①就是没有用户自定义的内存不足处理函数,在if(0 == __my_malloc_handler)这个if语句里面抛出异常;
第二个②退出循环条件就是在不断调用用户自定义内存不足函数之后,分配到了指定大小的内存,返回指向该内存区域的首地址

#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)  
  
static void* allocate(size_t __n)  
{  
    void* __result = malloc(__n);      //调用malloc()分配内存,向 system heap 要求空间  
    if (0 == __result) __result = _S_oom_malloc(__n);     //如果malloc分配失败,调用_S_oom_malloc()  
    return __result;                                      
}  
  
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG  
template <int __inst>  
void(*__malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;  
#endif                       
//内存不足处理例程,初值为0,待用户自定义,考虑内存不足时的应变措施。  
  
template <int __inst>  
void*  
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)  
{  
    void(*__my_malloc_handler)();    //函数指针  
    void* __result;  
  
    for (;;) {   //不断的尝试释放、配置、再释放、再配置……  
        __my_malloc_handler = __malloc_alloc_oom_handler;  
        /*由于初值设定为0,如果用户没有自定义相应的内存不足处理例程,那么还是抛出异常*/  
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }    
        (*__my_malloc_handler)();          //用户有自定义(释放内存),则进入相应的处理程序  
        __result = malloc(__n);              
        if (__result) return(__result);  
    }  
    //不断的尝试释放和配置是因为用户不知道还需要释放多少内存来满足分配需求,只能逐步的释放配置  
}  

但是这边仍然会有一个问题或许可以考虑,就是即便有用户自定义的内存不足处理函数,也不是一致调用它就可以不断释放空间,万一极端情况,系统就是一点空间也释放不出来了,那么就真的会成为一个死循环,所以其实可以进行防范以下这种情况,比如判断一下本次函数有没有运行清理出空间,如果有继续循环,否则可能就可以抛出异常了

reallocate()函数

reallocate函数的内部运行过程和allocate函数的过程是相似的,只不过把malloc换成了realloc,oom_allocate换成了oom_reallocate,过程都是一样的, (那为什么有了allocate还要有reallocate?)(一个是配置,一个是重配置,old_sz,new_sz)

static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)  
{  
    void* __result = realloc(__p, __new_sz); //reallocate 
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);  //获取失败,去oom
    return __result;  
}  
  
template <int __inst>  
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, 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 = realloc(__p, __n);  
        if (__result) return(__result);  
    }  
}  

deallocate

就是简单地封装了一个free

static void deallocate(void* __p, size_t /* __n */)  
{  
    free(__p);        //第一级配置器直接使用free()  
}  

⭐:一级空间配置器以malloc()free()realloc()等C函数执行实际的内存配置,释放,重配置的操作,因为SGI不能直接使用C++的set_new_handler(), 我们就必须仿真出一个set_malloc_handler(),该函数接收一个返回值为空,参数为空的函数指针作为参数,最后返回一个返回值和参数均为空的函数

/*该函数接收一个返回值为空,参数为空的函数指针作为参数,最后返回一个返回值和参数均为空的函数指针*/  
static void (* __set_malloc_handler(void (*__f)()))()     
{  
    void (* __old)() = __malloc_alloc_oom_handler;       //保存原有处理例程  
    __malloc_alloc_oom_handler = __f;                    //重新指定异常处理例程  
    return(__old);  
}  

ps:分配内存时,如果每次new出来 ,都要判断是否成功(地址是否为空),比较繁琐。
c++提供set_new_handler,当new失败时,会调用set_new_handler设置的回调函数, 设置为当前处理函数并返回原来的处理函数
set_malloc_handler()就是set_new_handler()的作用,就是用来处理内存不足准备的,它重新指定内存分配异常处理函数并返回原有的内存分配处理函数,为什么要保存并返回旧的处理函数,是方便以后又想使用;不断调用这个内存不足处理函数,希望某次调用之后可以获得我们申请大小的内存,正如一开始将的,这个函数中怎么释放空间是必须由自己定义方法的。

二级空间配置器__default_alloc_template 理论pa

why need 二级空间配置器:当频繁地在内存中开辟出不连续的小块内存时,会出现明明剩余空间够,但却因为剩余空间不连续导致无法开辟出内存的现象,也就是所谓的外碎片问题;另一方面,大量的小区间也会使得操作系统用来记录内存状态的数据结构很臃肿

以上,第二级内存配置器所采取的策略是,在第一次申请小内存时,先申请一块大内存留作备用,之后再申请小内存时,直接从上次申请的那一大块内存中划去要求的部分,不用再向系统申请。

同样的,第二级空间配置器提供了标准接口allocate()deallocate()reallocate()三个接口,在开始这三个接口之前先对一些相关专业名词了解一下:

  1. 内存区块,有时也称区块
    内存区块是指一块小内存,它的大小均为8的倍数,最大为128Bytes,即有8、16、24、32、40、48、56、64、72、80、88、96、114、122、128这几种,内存区块有自己的首地址,可以存储数据在每个区块的前8个字节,存储下一个可用区块的地址,通过这种方式,可以形成一条区块链表
  2. freelist数组
    内涵16个元素的数组,每一个元素是一个区块链表的首指针
  3. 内存池
    内存池是是一大块内存,它有三个参数:起始地址,终止地址以及大小,内存池的大小=终止地址 - 起始地址

在初始状态下,内存池是空的,内存区块也是不存在的,freelist数组中保存的都是空指针。我们从这种状态下开始分析,该机制是如何运作的。

运行过程

当申请的内存大于128bytes时,直接转交第一级配置器进行内存申请。

当申请的内存不大于128bytes时,则以内存池管理, 假设申请n字节:
1, 计算(n + 7)/7,得到一个整数值i,这个i即为freelist的元素索引
2,访问freelist位于i的元素,此时该元素为NULL,不指向任何可用区块(当区块已经交给客户端使用时, free-list就不再指向它们,即没有可用区块),这时将n向上调整为8的倍数,并调用refill函数

//n                  调整为8的倍数后申请的单个区块的大小
//返回值       该区块的地址
void* __default_alloc_template<threads,inst>::refill(size_t n);

3,refill的作用是给freelist重新填充内存区块(?),这些区块从内存池中获取,一次默认取20个,通过函数chunk_alloc获得

char* __default_alloc_template<threads,inst>::
chunk_alloc(size_t size , int& nobjs);
//size      申请的单个区块的大小
//nobjs   注意,nobjs是一个引用类型
//在refill调用这个函数时,传入的nobjs是20,即20个区块
//chunk_alloc执行完成后,nobjs会被修改为实际获得的区块数目

chunk_alloc函数返回的是一块长度为nobjs*n的内存块,refill函数需要将这一整块连续内存分割为一个个内存区块,并构建链表的连接关系

在内存充足的情况下,第一个内存块会被返回给用户使用,从第二块内存块开始构建链接关系;
在内存不足的情况下,假如只分配到了一个区块,则该区块直接交给用户使用,freelist不进行更新;
如果不足20个,则仍将获得的内存构建链接关系;
如果一个区块都没有获得,因为chunk_alloc函数内部调用了第一级配置器填充内存池,因此会按照第一级内存配置器的方式处理内存不足的情况。

chunk_alloc函数 P67~68⭐

char* __default_alloc_template<threads,inst>::
chunk_alloc(size_t size , int& nobjs);

需要关注的几个参数:
1,申请的内存总大小,size*nobjs,用total_bytes来表示
2,内存剩余空间,用bytes_left表示

如果total_bytes小于bytes_left,则直接划走total_bytes这么多内存,同时更新内存池的状态;

如果内存池的剩余空间不够申请的那么多区块,只够供应一部分区块,那么计算最多能划多少块,并划走;

如果连一个区块都无法供应,这时候就要给内存池“加水”:

  1. 首先要把内存池中剩下的水收集起来,别浪费了,加到freelist上去,具体的步骤是,根据剩下的内存的大小确定freelist的index,因为每个内存块都是8的倍数,划走时也按照8的倍数划分的,因此剩下来的内存一定可以构成一个内存区块,找到合适的freelist位置后,将这个区块加到freelist上,这时,就可以开始“加水”了
  2. 加水:利用malloc从heap中配置内存,为内存池注水以应付需求,且新水量为需求的两位,再加上一个随着配置次数增加而愈加增大的附加量(?)
    SGI STL选择的量是: 2 × total_bytes + heap_size >> 4 heap_size是以往内存池的容量的累加和,即附加量要满足,随着“加水”次数变多,每次加水的量应该越来越大这个条件
    3.确定加多少水后,通过malloc函数获取内存:
    ①如果获取成功,则更新内存池的状态,并递归调用chunk_malloc,因为内存池已经充足,下一次能够直接获取指定的内存
    ②如果没能获取那么多内存,首先,遍历freelist,如果freelist里面有大小大于一个size的空闲区块,则将这个区块加入到内存池,并递归,
    注意,这里的遍历并不是那种从freelist第一个开始逐个检查,而是以size为起点,确定freelist中相应的index,如果该index不含有空闲区块,则将size增加8字节,也就是检查下个freelist,直到后面的freelist都检查完,中途找到任何一个空闲区块,都会立即返回,不再遍历;
    ③如果遍历freelist也找不到足够的空闲区块,那么只能指望第一级配置器中由用户设置的内存不足处理函数能否解决,这里转交给第一级空间配置器,这时,要么第一级空间配置器顺利获得内存,这时会更新内存池,并递归,没能顺利获得内存,则会抛出异常。

释放内存

释放内存的过程相对简单,由第二级内存配置器分配的内存,在释放时并不交由free函数进行释放,也不放到内存池中,而是把内存加入到freelist链表中,以备下次使用,这个过程主要是简单的链表操作

内存区块链表 freelist

freelist中,每一个元素都是obj*,obj的结构

union obj
{
          union obj* free_list_link;//union
          char client_data[1];
}

它是内存区块的链表节点,需要记录当前区块的地址,以及下一个区块的地址,每个地址都是8个字节的指针,用一个struct来表示,需要16字节,而使用union结构,只需要8字节(?)

在每个内存区块的前8个字节处,是个obj对象,它存储着下一个内存区块的地址,有效区间为这8个字节,client_data是一个长度为1的数组,只有一个元素,它就是内存区块的第一个字节,为这个字节定义一个变量,并对它取址,得到的就是当前区块的地址。

这里采用数组的形式而不是直接定义一个char,目的是直接将client_data作为数组首地址返回,而不需要调用取址运算符,将该内存区块返回时,返回client_data,无须进行类型转换,直接在union中切换就行,状态的改变不会改变前8个字节的内容,但内存区块交出去后,前八个字节的内容丢失也不重要了,在将内存区块加入到freelist中时,会重新设置前8个字节的值,保证数据的有效性。
(为了维护链表,每个节点都需要额外的指针,为了解决这种负担,用union:当节点所指的内存块是空闲块,obj被视为一个指针,指向另一个节点,当节点已经被分配时,被视为一个指针指向实际区块)

总结一下,二级配置器的运行

STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)_第3张图片

  1. 如果想申请32个bytes时,找到free_list的3号下标,从里面拿掉第一个内存块返回给用户,然后让下标元素的值指向拿走的内存块的下一个内存块,如果是空,那么没有内存可用。
  2. 如果想申请24bytes,找到2号下标,但是2号下标下并没有内存块,这时候系统就会直接分20个对应大小的内存块,全部挂到这个下标位置,当下次申请该大小内存就和情况1一样
  3. 如果想申请25bytes,就申请32bytes(内碎片)

二级空间配置器__default_alloc_template 代码pa

二级空间配置器主要函数声明和具体框架

①定义全局的小型区块的上调边界和小型区块的上限以及free_list的个数
②round_up():将所申请的字节数上调为8的倍数
③free_list的节点构造
④配置一大块空间的refill(),可容纳nobjs个大小为size的区块
⑤chunk_alloc()函数,内存池的起始位置和结束位置都只在chunk_alloc()中改变
⑥allocate()、deallocate()、reaoolocate()
⑦最后是static data member的定义

template <bool threads, int inst>  
class __default_alloc_template {  
  
private:  
  //effective C++ 中条款二:  尽量使用const enum inline替换#define.
# ifndef __SUNPRO_CC  
    enum {__ALIGN = 8};  
    enum {__MAX_BYTES = 128};  
    enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  
# endif  
  static size_t ROUND_UP(size_t bytes) {  
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));  
  }  
__PRIVATE:  
  union obj {  
        union obj * free_list_link;  
        char client_data[1];    /* The client sees this. */  
  };  
private:  
# ifdef __SUNPRO_CC  
    static obj * __VOLATILE free_list[];   
        // Specifying a size results in duplicate def for 4.1  
# else  
    static obj * __VOLATILE free_list[__NFREELISTS];   
# endif  
  static  size_t FREELIST_INDEX(size_t bytes) {  
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);  
  }  
  
  // Returns an object of size n, and optionally adds to size n free list.  
  static void *refill(size_t n);  
  // Allocates a chunk for nobjs of size "size".  nobjs may be reduced  
  // if it is inconvenient to allocate the requested number.  
  static char *chunk_alloc(size_t size, int &nobjs);  
  
  // Chunk allocation state.  
  static char *start_free;  
  static char *end_free;  
  static size_t heap_size;  
  
 /* n must be > 0      */  
  static void * allocate(size_t n){...}  
  
 /* p may not be 0 */  
  static void deallocate(void *p, size_t n){...}  
 static void * reallocate(void *p, size_t old_sz, size_t new_sz);  
  
template <bool threads, int inst>  
char *__default_alloc_template<threads, inst>::start_free = 0;//内存池起始位置  
  
template <bool threads, int inst>  
char *__default_alloc_template<threads, inst>::end_free = 0;//内存池结束位置  
  
template <bool threads, int inst>  
size_t __default_alloc_template<threads, inst>::heap_size = 0;  
template <bool threads, int inst>  
__default_alloc_template<threads, inst>::obj * __VOLATILE  
__default_alloc_template<threads, inst> ::free_list[  
# ifdef __SUNPRO_CC  
    __NFREELISTS  
# else  
    __default_alloc_template<threads, inst>::__NFREELISTS  
# endif  

allocate函数的源码

如果申请数大于128则调用一级空间配置器,防止线程资源竞争要定义个锁
定位到我们想申请的在free_list上的位置,如果对应的内存大小的下标位置有内存块的话直接拿走内存块,没有的话就要refill去申请
放上之后要更新链表的连接状态

static void * allocate(size_t n)
{
	obj * __VOLATILE * my_free_list; //遇到VOLATILE关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
	obj * __RESTRICT result;
 
	if (n > (size_t)__MAX_BYTES) {
		return(malloc_alloc::allocate(n)); //调用一级空间配置器
	}
 
	my_free_list = free_list + FREELIST_INDEX(n);
	//FREELIST_INDEX(n)函数是用来获取free_list下标的,一会会提到的.
#       ifndef _NOTHREADS
	
	//防止线程资源竞争,加锁.
	lock lock_instance;
#       endif
 
	//这个过程是将头部的内存空间返回给用户,然后让下标元素的值指向下面的内存块
	//类似链表的头删过程,不过是将拿掉的内存块返回给用户了.
	//refill(ROUND_UP(n)) 是用来向系统内存申请空间的.
	//ROUND_UP(n) 这个函数是为了让n和8的倍数对齐. 申请空间单个的单位一定是free_list中对应的内存块大小.
	result = *my_free_list;
	if (result == 0) {
		void *r = refill(ROUND_UP(n));
		return r;
	}
	*my_free_list = result->free_list_link;
	return (result);
};

refill()函数源码

如果对应大小下标位置没有一个内存块时就需要调用refill函数向系统申请内存

每次refill的时候,二级空间配置器希望直接向内存申请20块相同大小的内存块,这样就不用反复的调用自己;为了提高效率,定一个nobjs,将它传给chunk_alloc函数,具体申请空间就让chunk_alloc函数去做,refill函数就专心做链接内存块的事情

如果chunk_alloc函数只申请到一个内存块,那么直接返回;如果不止一个,那么要进行内存链接,第一个区块是直接返回给用户的

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
	int nobjs = 20;
	char * chunk = chunk_alloc(n, nobjs);//申请20个n大小的内存块
	obj * __VOLATILE * my_free_list;
	obj * result;
	obj * current_obj, *next_obj;//用于连接
	int i;
 
	//如果chunk_alloc函数只申请到一个内存块,那么直接返回.
	if (1 == nobjs) return(chunk);
 
	//chunk_alloc申请的内存块不止一个,可以进行链接内存块.
	my_free_list = free_list + FREELIST_INDEX(n);
 
	//下面的过程是链接内存块的过程.
	//这里的my_free_list和next_obj 都是直接指向chunk+n的位置的. 因为第一个内存块需要返回给用户使用.
	result = (obj *)chunk;
	*my_free_list = next_obj = (obj *)(chunk + n);
 
	for (i = 1;; i++) { //i=0的那部分已经为用户准备好了是要返回的空间.
		current_obj = next_obj;
		next_obj = (obj *)((char *)next_obj + n);
		if (nobjs - 1 == i) {
			current_obj->free_list_link = 0;
			break;
		}
		else {
			current_obj->free_list_link = next_obj;
		}
	}
	return(result);
}

把连接内存块的过程摘出来看看:

		my_free_list = free_list + FREELIST_INDEX(n);//收获了不止一个区块,要做调整纳入新节点
	result = (obj *)chunk;//这是第一块内存地址,要返回的结果
	*my_free_list = next_obj = (obj *)(chunk + n);//将free_list指向新配置的空间, n是每块内存块的字节大小
 
 //第1个返回之后,从第二个开始进行连接并更新指针,之后又要取内存块的话是从当前的第二个开始的
	for (i = 1;; i++) { //i=0的那部分已经为用户准备好了是要返回的空间.
		current_obj = next_obj;
		next_obj = (obj *)((char *)next_obj + n);
		if (nobjs - 1 == i) {
			current_obj->free_list_link = 0;
			break;
		}
		else {
			current_obj->free_list_link = next_obj;
		}
	}
	return(result);
}

其中ROUND_UP将n保证为8的倍数并且这个数大于等于n;FREELIST_INDEX在free_list中找到相应的下标

static size_t ROUND_UP(size_t bytes) {
		return (((bytes)+__ALIGN - 1) & ~(__ALIGN - 1));
	}
static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

chunk_alloc源码

当freelist中需要的对应内存大小下标位置没有一个内存块时就需要向系统申请内存,我们知道用的时refill()但实际的从内存池中取具体空间是chunk_alloc函数去做的
那么关于具体申请空间:
①当系统剩余的内存大于希望申请的20个相同内存块空间时,就直接划走
②当系统中剩余空间不能完全满足需求量,但足够供应一个(或大于一个)以上的区块,就能划走多少划走多少,更新返回的节点数就ok
③如果内存池连一个区块都无法给客户端提供,那么就得用malloc申请内存,申请2倍需求量+附加量的内存;在申请之前先将内存池中的一些残余零头加入到free_list中,利用FREELIST_INDEX()找到适当的free list,加入它并调整连接
④如果heap也没有内存了配置失败,那就回到free_list中寻找是否有大于单个区块的内存块还没有使用,如果有的话就释放内存,再挂到申请的区块位置上
⑤还是失败的话,就调用一级空间配置器,尝试用oom机制

template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)
{
	char * result;
	size_t total_bytes = size * nobjs;
	size_t bytes_left = end_free - start_free;
 
	//当系统中剩余的内存大于你申请20个相同内存块的空间.
	if (bytes_left >= total_bytes) {
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
	//当系统中剩余的内存大于一个你申请的内存块.
	else if (bytes_left >= size) {
		nobjs = bytes_left / size;
		total_bytes = size * nobjs;
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
 
	//
	else {
		//如果内存池连一个区块都无法给客端提供,就调用malloc申请内存,新申请的空间是需求量的两倍
		//与随着配置次数增加的附加量. 在申请之前,将内存池的残余的内存收回.
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		if (bytes_left > 0) {
			obj * __VOLATILE * my_free_list =
				free_list + FREELIST_INDEX(bytes_left);
 
			((obj *)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *)start_free;
		}
		
		
		start_free = (char *)malloc(bytes_to_get);//配置heap空间,用来补充内存池
		 //if (0 == start_free)
		//如果没有申请成功,如果free_list当中有比n大的内存块,这个时候将free_list中的内存块释放出来.
		//然后将这些内存编入自己的free_list的下标当中.调整nobjs.
		if (0 == start_free) {
			int i;
			obj * __VOLATILE * my_free_list, *p;
 
			for (i = size; i <= __MAX_BYTES; i += __ALIGN) {//从size开始找
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				if (0 != p) {//如果还有尚未使用区块,就给释放了
					*my_free_list = p->free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					return(chunk_alloc(size, nobjs));
					// Any leftover piece will eventually make it to the
					// right free list.
				}
			}
			end_free = 0;	// In case of exception.
 
			//这个是个就是弹尽粮绝了,去求求一级空间配置器来帮帮忙. 看看oom机制是否能过来帮忙
			//这样会有两种可能,一种是抛出ban_alloc异常 一种是开辟空间成功.
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
		}
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		return(chunk_alloc(size, nobjs));
	}
}

⭐⭐⭐情况总结↓

STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)_第4张图片

deallocate()释放空间源码

  1. 如果需要回收的区块大于128bytes,则调用第一级空间配置器
  2. 如果需要回收的区块小于128bytes,则找到free_list当中该块空间的大小的位置,然后将区块回收. (这里的回收其实就是头插):还是用FREELIST_INDEX()找到属于哪个区块,然后头茬
/* p cannot be 0 */
static void deallocate(void *p, size_t n)
{
	obj *q = (obj *)p;
	obj * __VOLATILE * my_free_list;
 
	if (n > (size_t)__MAX_BYTES) {//大于128, 去一级
		malloc_alloc::deallocate(p, n);
		return;
	}
	my_free_list = free_list + FREELIST_INDEX(n);
	// acquire lock
#       ifndef _NOTHREADS
	/*REFERENCED*/
	lock lock_instance;
#       endif /* _NOTHREADS */
 
	//很标准的一个头插
	q->free_list_link = *my_free_list;
	*my_free_list = q;
	// lock is released here
}

总结❗

STL——空间配置器剖析(一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的)_第5张图片

二级空间配置器的缺陷与问题

可参考https://blog.csdn.net/dawn_sf/article/details/78774275

  1. 二级空间配置器从头到尾都没有看到它释放内存,究竟是否释放,合适释放:
    答:二级配置器并没有将申请的空间释放,而是将它们挂在了自由链表上,空间配置器的所有方法,成员都是静态的, 那么它们就存放在静态区,因此释放的实际必定也是程序结束时

  2. 自由链表释放空间的连续性问题
    真正在程序中就归还空间的只有自由链表中的未使用值,由于用户申请空间、释放空间顺序的不可控性,这些空间并不一定是连续的,而释放空间必须保证其连续性。保证连续的方案可以是:跟踪分配释放过程、记录节点信息,释放时,仅释放连续的大块空间。(倒是没咋看懂)

  3. 二级空间配置器的效率问题
    二级配置器虽然解决了外碎片的问题,但同时也造成了内存片。如果用户频繁申请char类型的空间,而配置器默认对其到8的倍数,那么剩下的7/8的空间就会浪费。如果用户频繁申请8字节的空间,甚至将堆中的可用空间全部挂在了自由链表的第一个节点,这时如果申请一个16字节的内存,也会失败。这也是二级配置器的弊端所在,设置一个释放内存的函数是很有必要的

你可能感兴趣的:(STL源码)