一级空间配置器、二级空间配置器的本质及运用场合,是如何用内存池去管理的
研究了好久才写好的,主要是二级配置器,大标题小标题什么的可能没有安排好,先
写了原理上的内容,再剖析了各个函数源码,各个目录可以看csdn自带的目录(
每个容器都会通过默认参数指定好allocator,不需要显示声明,如定义一个vector类容器:std::vector
而完整的vecTemp声明应该是vector
加入自定义了将内存分配指向磁盘或者其他存储介质空间的allocator,那么只要在声明时传入设计好的allocator,不再使用默认的allocator就行了
那么,如何设计一个allocator呢?
一个空间配置器最基本的功能有四:申请内存、构造对象、析构对象、释放内存;
但是我们想要设计一个在STL中用的空间配置器的话,光有上述四个是不行的,因为STL对allocator的组成已经规定好了,即有STL规范。
由于一个内存配置与释放通常分两个阶段,而STL allocator 将这两个阶段的操作区分开来:
alloc::allocate()
负责,内存释放由alloc::deallocate()
负责::construct()
负责,对象析构由::destroy()
负责allocator::allocate()
和allocator::deallocate()
, 但它就是对::operator new
和::operator delete
做了一层薄薄的封装, 效率不好就不要使用了STL标准规格告诉我们,配置器定义于
中, SGI 包含以下两个文件:
1, #include
//负责内存空间的配置与释放
2, #include
//负责对象内容的构造与析构
书P52~53, 理解是简单的
construct()就是接受一个指针p和一个初值value,用途就是将初值设定到指针所致的空间上,可以通过placement new来完成。
destroy()有两个版本:第一个版本:接受一个指针,直接调用析构函数即可;第二个版本:接受一个迭代器区间,将区间范围内的对象析构掉
考虑到小型区块所可能造成的内存破损问题,SGI设计了双层级配置器
分界点是配置的内存是否大于128B,大于就用第一级配置器直接使用malloc()和free()
,小于等于则通过第二级访问复杂的memory pool
整理方式。
通过是否定义_USE_MALLOC
宏,来设定是只打开第一级还是同时打开第一级与第二级。SGI STL没定义那个宏,也就是同时开放一、二级。
一级空间配置器就是更大程度来合理运用空间,它的内部设计实际就是为了压榨剩余的内存,达到内存的高效运用, 一级空间配置器内部其实就是malloc
和free
的封装,然后尽量开辟想要的内存空间,==就算系统内部的剩余内存空间小于你所申请的内存空间,==它都会努力尝试开辟出来
一级空间配置器的声明:
/*__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
理论pawhy need 二级空间配置器:∵当频繁地在内存中开辟出不连续的小块内存时,会出现明明剩余空间够,但却因为剩余空间不连续导致无法开辟出内存的现象,也就是所谓的外碎片问题;另一方面,大量的小区间也会使得操作系统用来记录内存状态的数据结构很臃肿
以上,第二级内存配置器所采取的策略是,在第一次申请小内存时,先申请一块大内存留作备用,之后再申请小内存时,直接从上次申请的那一大块内存中划去要求的部分,不用再向系统申请。
同样的,第二级空间配置器提供了标准接口allocate()
、deallocate()
、reallocate()三个接口
,在开始这三个接口之前先对一些相关专业名词了解一下:
在初始状态下,内存池是空的,内存区块也是不存在的,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函数内部调用了第一级配置器填充内存池,因此会按照第一级内存配置器的方式处理内存不足的情况。
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这么多内存,同时更新内存池的状态;
如果内存池的剩余空间不够申请的那么多区块,只够供应一部分区块,那么计算最多能划多少块,并划走;
如果连一个区块都无法供应,这时候就要给内存池“加水”:
malloc
从heap中配置内存,为内存池注水以应付需求,且新水量为需求的两位,再加上一个随着配置次数增加而愈加增大的附加量(?)2 × total_bytes + heap_size >> 4
heap_size是以往内存池的容量的累加和,即附加量要满足,随着“加水”次数变多,每次加水的量应该越来越大这个条件chunk_malloc
,因为内存池已经充足,下一次能够直接获取指定的内存释放内存的过程相对简单,由第二级内存配置器分配的内存,在释放时并不交由free函数进行释放,也不放到内存池中,而是把内存加入到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被视为一个指针,指向另一个节点,当节点已经被分配时,被视为一个指针指向实际区块)
__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
如果申请数大于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的时候,二级空间配置器希望直接向内存申请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);
}
当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));
}
}
⭐⭐⭐情况总结↓
deallocate()
释放空间源码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
}
可参考https://blog.csdn.net/dawn_sf/article/details/78774275
二级空间配置器从头到尾都没有看到它释放内存,究竟是否释放,合适释放:
答:二级配置器并没有将申请的空间释放,而是将它们挂在了自由链表上,空间配置器的所有方法,成员都是静态的, 那么它们就存放在静态区,因此释放的实际必定也是程序结束时
自由链表释放空间的连续性问题
真正在程序中就归还空间的只有自由链表中的未使用值,由于用户申请空间、释放空间顺序的不可控性,这些空间并不一定是连续的,而释放空间必须保证其连续性。保证连续的方案可以是:跟踪分配释放过程、记录节点信息,释放时,仅释放连续的大块空间。(倒是没咋看懂)
二级空间配置器的效率问题
二级配置器虽然解决了外碎片的问题,但同时也造成了内存片。如果用户频繁申请char类型的空间,而配置器默认对其到8的倍数,那么剩下的7/8的空间就会浪费。如果用户频繁申请8字节的空间,甚至将堆中的可用空间全部挂在了自由链表的第一个节点,这时如果申请一个16字节的内存,也会失败。这也是二级配置器的弊端所在,设置一个释放内存的函数是很有必要的