JoeChenzzz

导航

空间配置器

1.为什么需要空间配置器

1.1内存碎片

  从内存分配的角度来看,我们不免因为程序需求频繁申请、释放小块内存,从而在堆中造成外碎片,外碎片是指系统中空闲内存总量足够,但是不连续,所以无法分配给用户使用

注:内碎片是指已经分配给用户,用户却不利用的内存。如用户需要3字节,实际却得到了4字节,其中的1字节是浪费掉的

1.2频繁向系统申请小块空间,效率低

  内存空间是由操作系统管理的,当我们要去开辟时,调用malloc( ),要进行用户态/内核态的切换,这样系统调用会产生性能问题,频繁地因为很小一块内存就进行系统调用,效率很低

2.空间配置器的实现

2.1整体策略

  STL认为大于128字节的内存为大块内存,小于等于128字节的内存为小块内存,当请求的大块内存时,就使用第一层配置器分配内存,请求的小块内存则调用第二层配置器分配

2.2一级空间配置器

1)一级空间配置器的allocate()、deallocate()、reallocate()封装了malloc()、 free()、 realloc()等C库函数执行分配、释放、重新配置内存等操作

2)此外,为了处理内存不足的状况,一级空间配置器增加handle处理机制:在内存请求无法被满足时,调用设定的函数

3)一级空间配置器的allocate()如果调用malloc()分配内存不成功,将会调用内存不足处理函数oom_malloc(),oom_malloc()会循环调用错误处理函数__malloc_alloc_oom_handler,企图释放内存,再重新调用malloc(),直到分配成功;如果未设定错误处理函数,将直接bad_alloc异常信息

template <int inst>
class __malloc_alloc_template
{
private:
    static void *oom_malloc(size_t);   //使用malloc调用的内存不足处理函数
    static void *oom_realloc(void *, size_t); //使用realloc调用的内存不足处理函数
    static void(*__malloc_alloc_oom_handler)();     //错误处理函数
public:
    static void * allocate(size_t n)                        {
        void *result = malloc(n);                     //一级空间配置器直接调用malloc
        if (0 == result)
            result = oom_malloc(n);
        return result;
    }

    static void deallocate(void *p, size_t /* n */)
    {
        free(p);
    }

    static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
    {
        void * result = realloc(p, new_sz);           //一级空间配置器直接调用realloc
        if (0 == result)
            result = oom_realloc(p, new_sz);
        return result;
    }

    static void(*set_malloc_handler(void(*f)()))()    //设置错误处理函数
    {
        void(*old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return(old);
    }
};

template <int inst>
void * __malloc_alloc_template<inst>::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);
    }
}

2.3二级空间配置器

1)二级空间配置器使用内存池+自由链表的机制:内存池负责向系统申请内存,分配小内存块给自由链表,自由链表共有16条,存储在数组free_list中,数组中由前往后的链表分别负责8字节,16字节,24字节,…,120字节,128字节的内存请求。为了便于管理,二级空间配置器在分配的时候字节数都是以8的倍数对齐,当内存请求字节数不是8的倍数时,将自动向上调整至8的倍数。当所需内存 小于等于128字节时,则去对应负责的自由链表上取内存块,当这块内存用完了,直接放回链表上即可回收;如果在申请内存时链表上没有内存块了,则向内存池申请内存块,此时会申请 nobjs 个(默认为20),将第1个返回给申请者,剩余的挂到对应链表上

2)为了节省内存,将不使用额外的指针串连自由链表上的节点,而是采用一物两用的方法:节点是一个union,由于union的特性,_freeListLink指针可以指向下一个节点,_clientData又可以是内存块的首地址,也就是使用内存块提供的便利来存放下一个节点的地址,把节点串起来形成链表

union _Obj                      //自由链表的节点
{
    _Obj* _freeListLink;         //指向自由链表节点的指针
    char _clientData[1];          //内存块首地址
};

3)内存池分配内存块给自由链表的过程(函数_chunkAlloc):

  • 当内存池剩余的空间大小 leftBytes >= n x nobjs时,则直接分配好返回
  • 当内存池剩余的空间大小 leftBytes 的范围是 [ n x 1, n x nobjs ),则这时候就分配 nobjs = leftBytes / n 这么多块的内存块返回
  • 当内存池剩余的空间大小 leftBytes < n x 1 时,则先将剩余空间挂到自由链表上,再调用malloc()向系统申请 2 x n x nobjs + _GetRoundUp(_heapSize / 16) 字节的新内存。若申请成功,则再调用一次_chunkAlloc给自由链表分配内存块;若失败,则先去自由链表上找一块比 n 大的内存块,将它摘下来还给内存池,然后再调用一次_chunkAlloc分配内存,要是没找到,就调用一级空间配置器,看看内存不足处理机制能否处理

enum { _ALIGN = 8 };              //按照基准值8的倍数进行内存操作
enum { _MAXBYTES = 128 };        //自由链表中最大的块的大小是128
enum { _NFREELISTS = 16 };       //自由链表数组的长度,等于_MAXBYTES/_ALIGN
template <bool threads, int inst>  //非模板类型参数
class _DefaultAllocTemplate
{
    union _Obj                      //自由链表的节点
    {
        _Obj* _freeListLink;         //指向自由链表节点的指针
        char _clientData[1];          //内存块首地址
    };
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<bool threads, int inst>
char* _DefaultAllocTemplate<threads, inst>::_startFree = 0;        //内存池的头指针
template<bool threads, int inst>
char* _DefaultAllocTemplate<threads, inst>::_endFree = 0;           //内存池的尾指针
template<bool threads, int inst>
size_t _DefaultAllocTemplate<threads, inst>::_heapSize = 0;              //记录内存池已经向系统申请了多大的内存
template<bool threads, int inst>
typename _DefaultAllocTemplate<threads, inst>::_Obj* volatile      //前面加typename表示后面是个类型
_DefaultAllocTemplate<threads, inst>::_freeList[_NFREELISTS] = { 0 };    //自由链表

//分配空间
template<bool threads, int inst>
void* _DefaultAllocTemplate<threads, inst>::Allocate(size_t n)    //n:要申请的字节数
{
    void *ret;
    //大于_MAXBYTES(128)个字节则认为是大块内存,直接调用一级空间配置器
    if (n > _MAXBYTES)      
    {
        ret = malloc_alloc::_Allocate(n);
    }
    else       //否则就去自由链表中找
    {
        _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n);  //让myFreeList指向自由链表中n向上取8的整数倍
        _Obj* result = *myFreeList;
        if (result == NULL)  //如果这条链表上没有挂内存块,则就要去内存池中申请
        {
            ret = _Refill(_GetRoundUp(n));      //到内存池中申请
        }
        else            //已经在自由链表上找到了内存块
        {
            *myFreeList = result->_freeListLink;      //把第2个内存块的地址放到自由链表上
            ret = result;    //第1个作为返回值
        }
    }
    return ret;
}

//回收空间
template<bool threads, int inst>
void _DefaultAllocTemplate<threads, inst>::DeAllocate(void *p, size_t n)    //n:要申请的字节数
{
    //如果n大于128,就直接调用一级空间配置器的释放函数
    if (n > _MAXBYTES)  
    {
        malloc_alloc::_DeAllocate(p);
    }
    else //否则将这块内存回收到自由链表中
    {
        _Obj* q = (_Obj*)p;
        _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n);
        q->_freeListLink = *myFreeList;
        *myFreeList = q;
    }
}

//自由链表向内存池取内存
template<bool threads, int inst>
void* _DefaultAllocTemplate<threads, inst>::_Refill(size_t n)    //n表示要申请的字节数
{
    int nobjs = 20;           //默认向内存池一次性申请20块
    char* chunk = _chunkAlloc(n, nobjs);    //调用_chunkAlloc()向内存池申请,nobjs是引用传参,可以改变
    if (1 == nobjs)          //如果内存池只分配了1块,则直接返回给调用者
    {
        return chunk;
    }
    //如果分配了多块,则返回第1块给调用者,其他挂在自由链表上
    _Obj* ret = (_Obj*)chunk;    //将第1块作为返回值
    _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(n);
    *myFreeList = (_Obj*)(chunk + n);  //将第2块的地址放到自由链表上
    _Obj* cur = *myFreeList;
    _Obj* next = NULL;
    for (int i = 1; i < nobjs; ++i)        //将剩下的块挂到自由链表上
    {
        next = (_Obj*)((char*)cur + n);
        cur->_freeListLink = next;
        cur = next;
    }
    cur->_freeListLink = NULL;
    return ret;
}

//内存池向系统申请内存
//size:小内存块的字节数,nobjs:要申请的块数
template<bool threads, int inst>
char* _DefaultAllocTemplate<threads, inst>::_chunkAlloc(size_t size, int& nobjs)  
{
    char* result = NULL;
    size_t totalBytes = size * nobjs;        //所请求的内存大小
    size_t leftBytes = _endFree - _startFree;      //内存池剩余的大小

    if (leftBytes >= totalBytes)     //内存池剩余大小足够分配nobjs块
    {
        result = _startFree;
        _startFree += totalBytes;
        return result;
    }
    else if (leftBytes >= size)    //内存池剩余大小不够分配nobjs块,但至少够1块
    {
        nobjs = (int)(leftBytes / size);
        result = _startFree;
        _startFree += (nobjs*size);
        return result;
    }
    else    //内存池剩余大小连1块都不够分配了
    {
        if (leftBytes > 0)  //把内存池的零头挂到自由链表上
        {
            _Obj* volatile *myFreeList = _freeList + _GetFreeListIndex(leftBytes);
            ((_Obj*)_startFree)->_freeListLink = *myFreeList;
            *myFreeList = (_Obj*)_startFree;
        }
        //内存池调用malloc()开辟新内存,一次性开辟
        size_t NewBytes = 2 * totalBytes + _GetRoundUp(_heapSize >> 4);       
        _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;

3.空间配置器存在的问题

1)内碎片:容易产生内碎片,自由链表上所挂的内存块的大小都是8字节的整数倍,因此当我们需要非8倍数的内存块,往往会导致浪费,比如我只要1字节,但是自由链表最低分配8字节,也就浪费了7字节。这一点在计算机科学中很常见
2)没有释放自由链表上所挂内存块的函数:空间配置器中所有的函数和变量都是 static 的,那么他们是存放在数据段的,又没有写释放他们的函数,所以在程序结束的时候才会释放他们,这样就导致自由链表一直占用着内存

posted on 2019-03-22 08:34  JoeChenzzz  阅读(275)  评论(0编辑  收藏  举报