STL-空间配置器分析(内存的配置和释放)

首先通过STL源码剖析中的一个插图有个初步的印象,接下来再详细解释。(由于是繁体,图中的记忆体也就是所谓的“内存池 ”)

这里写图片描述


一级空间配置器:

对malloc,free函数的简单封装;添加了增加_malloc_alloc_oom_handle处理机制对malloc失败后的情况进行了处理;

申请内存:

1、void* allocate(size_t n) :调用malloc函数,如果malloc失败,调用_S_oom_malloc(n)函数;否则malloc开辟成功返回;
2、void* reallocate(void* _p, size_t new_size):调用realloc函数,realloc失败,调用_S_oom_malloc(n)函数;否则realloc开辟成功返回;

释放内存:

void deallocate(void* _p):内部封装free函数。


一级配置器简化流程图:

这里写图片描述

简化后的源码分析如下:
template <int __inst> //预留,整个过程中都没有用到
class __malloc_alloc_template {
private:
    static void* _S_oom_malloc(size_t);     //malloc失败调用的函数
    static void(*__malloc_alloc_oom_handler)(); //用户自定义的处理内存不足的函数

public:
    //如果申请成功,直接返回
    //申请失败调用_S_oom_malloc处理函数
    static void* allocate(size_t __n)
    {
        void* __result = malloc(__n);   
        if (0 == __result) __result = _S_oom_malloc(__n);
        return __result;
    }

    //释放空间直接调用free即可
    static void deallocate(void* __p)
    {
        free(__p);
    }
};

// malloc_alloc out-of-memory handling
//初始值置为0,等待用户自定义释放内存函数
template <int __inst>
void(*__malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;


//之前malloc,realloc函数申请失败都会首先调用该函数
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void(*__my_malloc_handler)();
    void* __result;

    //如果用户自定义了__malloc_alloc_oom_handler函数,则一直调用直到内存申请成功
    //若未定义__malloc_alloc_oom_handler函数,则直接抛出异常。
    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);
    }
}

二级空间配置器:
二级空间配置器主要是为了解决外内存碎片的问题,所以引入了以下的结构:
一、整体结构:
由start_free和end_free共同维护的一片狭义内存池,以及一个自由链表,该自由链表其实是一个指针数组,每个元素都可指向了一段分配好的内存块;由于是SGI3.0版本的STL,年代较久;当时认为128字节就是大块内存,所以将128分为16块; 每个下标下方对应的都是对应内存大小块的内存。
每个自由链表的节点类型:
  union _Obj {
        union _Obj* _M_free_list_link;   /* 相当于是next指针 */
        char _M_client_data[1];    /* The client sees this.        */
  };
图示结构如下:
其中0,1,2代表的是下标,8,16,24则是该下标位置下挂的内存块所对应的大小

这里写图片描述


简化后源码分析如下(只考虑单线程情况下)
 enum {_ALIGN = 8}; //小块的上调边界
 enum {_MAX_BYTES = 128}; //小块边界的上限
 enum {_NFREELISTS = 16}; //first_list的个数

template<bool threads, int inst> 
//thread为true,代表多线程
class _DeafaultAllocTemplate
{
    //first-list的节点
    struct _Obj{
        _Obj* _freeListLink;    //相当于_next指针,指向下一块内存
    };

private:
    //狭义内存池  start和end只在chunk函数中会改变
    static char* _startFree;
    static char* _endFree;
    static size_t _heapSize;

    //自由链表
    static _Obj* _freeList[_NFREELIST];

private:

    //给申请内存块的大小,返回该大小在指针数组中对应的下标
    static size_t _GetFreeListIndex(size_t n)
    {
        //将整体值跃迁到下一个区间
        return ((n + _ALIGN - 1) / _ALIGN - 1);
    }

    //返回离n上调到最近的8的倍数 例如 n=7, return 8
    static size_t _RoundUp(size_t n)
    {
        return (n + _ALIGN - 1) & ~(_ALIGN - 1);
    }

    //在自由链表中获取内存块
    static void* _Refill(size_t n);

    //在内存池中申请内存nobjs个对象,每个对象内存大小为n,
    //nobjs为输出型参数,可用来判断实际开辟了几个内存块
    static char* _chunkAlloc(size_t n, int& nobjs);    

public:
    //申请内存
    static void* Allocate(size_t n);
    //释放内存
    static void* DeAllocate(void* p, size_t n);
};

二、关键成员函数分析:
<1>:Allocate函数:
判断所申请内存块大小是否大于128,如果大于128则直接调用一级空间配置;反之则将n上调至最近的8的倍数处,然后再去自由链表中相应的结点下面找,如果该结点下面还存在未使用的内存,则直接取下该块内存,返回内存地址;否则调用refill函数去内存池中申请,剩下的事就交给refill了。
static void* allocate(size_t __n)
{
    void* __ret = 0;

    //如果申请的内存块大小超过MAX_BYTES,就直接调用一级空间配置器
    if (__n > (size_t)_MAX_BYTES) {
        __ret = malloc_alloc::allocate(__n);
    }
    else 
    {
        //找到对应内存块大小的下标
        _Obj** __my_free_list = _S_free_list + _S_freelist_index(__n);
        _Obj* __result = *__my_free_list;
        //如果当前下标对应的链表中没有可用的内存块,就调用refill
        //refill函数会返回一块对应大小内存块的地址
        if (__result == 0)
            __ret = _S_refill(_S_round_up(__n));

        //有对应大小的内存块就直接从头上取一块返回
        else {
            *__my_free_list = __result->_M_free_list_link;
            __ret = __result;
        }
    }
    return __ret;
};

这里写图片描述

<2>:deallocate函数:
deallocate函数作用就是按照申请内存块的情况,将对应的内存块归还给不同的地方;更详细的解释见下面
static void deallocate(void* __p, size_t __n)
{
    //如果归还内存块的大小大于MAX_BYTES
    //则说明开辟的时候就调用一级空间配置器,所以直接交给一级处理
    if (__n > (size_t)_MAX_BYTES)
        malloc_alloc::deallocate(__p, __n);
    //剩下的情况就是在freelist上取的内存块,则需要找到对应位置进行头插
    else {
        _Obj* __my_free_list = _S_free_list + _S_freelist_index(__n);
        _Obj* __q = (_Obj*)__p;

        __q->_M_free_list_link = *__my_free_list;
        *__my_free_list = __q;
    }
}

这里写图片描述


<3>:refill函数:重新填充自由链表
STL默认一次申请nobjs=20个,将多余的挂在自由链表上,这样能够提高效率。进入refill函数后,先调chunk_alloc(size_t n,size_t& nobjs)函数去内存池中申请,如果申请成功的话,再回到refill函数。由于nobjs是输出型参数,在chunk函数内部会根据申请的大块内存大小,确定nobjs的值。
回到refill函数后就有两种情况,如果nobjs=1的话则表示内存池只够分配一个,这时候只需要返回这个地址就可以了。否则就表示nobjs大于1,则将多余的内存块挂到自由链表上。如果chunk_alloc失败的话,在该函数内部有处理机制。
template <bool __threads, int __inst>
void* __default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    //初始nobjs为20,最好的情况下能一次开辟20个对应大小的内存块;剩下情况再处理
    int __nobjs = 20;
    //chunk_alloc向内存池申请空间,但是能成功申请的块数不定,通过输出型参数nobjs确定
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj** __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    //如果只成功申请了一块,则直接返回该块地址
    if (1 == __nobjs) return(__chunk);

    //用申请来的大块内存往自由链表中挂内存块
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    //第一块记录下来返回
    __result = (_Obj*)__chunk;

    //将剩余的全部挂入链表下面
    *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
    for (__i = 1; ; __i++) {
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {
            __current_obj->_M_free_list_link = 0;
            break;
        }
        else {
            __current_obj->_M_free_list_link = __next_obj;
        }
    }
    return(__result);
}

<4>:chunk_alloc函数:处理内存池的问题,返回大块内存给refill函数使用。
在chunk_alloc函数中存在三种情况
1、内存池剩余空间足够开辟20* n的空间,直接分配返回给refill函数使用;
2、内存池剩余空间至少能够分配一个大小为n的大块内存,但是不够20个,重新计算nobjs,分配并返回;
3、内存池空间不足n,先将内存池剩余空间挂到对应自由链表中,再次malloc申请空间。
3.1若此时malloc成功,再次调用chunk_alloc分配内存返回;
3.2若malloc失败,去比当前n大的自由链表中找闲置的内存块:如果有,则将该块内存返回给内存池,再次调用chunk_alloc;否则调用一级空间配置器,期待内存不足处理机制能否处理。
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __nobjs;    //总申请字节大小
    size_t __bytes_left = _S_end_free - _S_start_free;  //内存池中剩余字节数

    //1、如果剩余内存大小够开辟20个所申请类型大小的空间
    //就返回此时的start_free,然后将start_free再向后挪动total——bytes
    if (__bytes_left >= __total_bytes) {
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    }

    //2、如果剩余大小不够开20个,但是最少能开一个
    //更新此时最多能开的nobjs,返回地址
    else if (__bytes_left >= __size) {
        __nobjs = (int)(__bytes_left / __size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    }

    //3、走到此时,说明内存池中连一个size大小的空间都申请不到
    else {
        //先计算出一会malloc的字节大小
        size_t __bytes_to_get =
            2 * __total_bytes + _S_round_up(_S_heap_size >> 4);

        // 将内存池剩余空间挂到对应的链表中
        if (__bytes_left > 0) {
            _Obj** __my_free_list =
                _S_free_list + _S_freelist_index(__bytes_left);

            ((_Obj*)_S_start_free)->_M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }

        //3、1此时内存池中也空了,直接进行malloc
        _S_start_free = (char*)malloc(__bytes_to_get);

        //3、2如果malloc失败,尝试去比当前内存块大的链表中取出一块先用 
        if (0 == _S_start_free) {
            size_t __i;
            _Obj** __my_free_list;
            _Obj* __p;

            for (__i = __size;
                __i <= (size_t)_MAX_BYTES;
                __i += (size_t)_ALIGN) {
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;
                //如果后续链表有空闲内存块,再次调用该函数,此时已经保证能开辟一个size大小的内存块
                if (0 != __p) {
                    *__my_free_list = __p->_M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                }
            }
            //此时内存空间十分吃紧
            //调用一级空间配置器,期待__malloc_alloc_oom_handler能够解决该问题。
            _S_end_free = 0;    
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
        }
        //malloc成功,再次向内存池申请空间
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

二级配置器简化流程图:

这里写图片描述


关于空间配置器的优缺点
优点:空间配置器的出现就是为了尽量避免外碎片的出现。(外碎片指:当系统中有n个字节的空间时,我们却不能开辟出来,就说明此时内存中存在很严重的外碎片问题);
缺点:空间配置器虽然避免了外碎片的出现,但是引入了内碎片的问题。(内碎片值:由于在自由链表中取内存块大小都是取的8的倍数,但是实际应用中可能只会用到前5个字节,而后三个字节就是引发的内碎片)

注:有部分截图来源于侯捷的STL源码剖析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值