STL内存管理详细分析

2019-10-29  本文已影响0人  earthwjl

        STL中内存管理非常精妙,本文以SGI STL为例,分析其内存管理的设计思路,也是对侯捷老师的《STL源码剖析》中相关内容的总结。
        首先,从总体上看,STL空间配置器分为两级,针对大内存的申请,调用第一级空间配置器,对于小内存的申请,则调用第二级配置器。

第一级空间配置器

        第一级空间配置器对外提供了allocate(),deallocate(),reallocate()三个函数供用户使用,同时,其内部定义了oom_allocate()和oom_reallocate()函数,用于处理内存不足的情况。
        deallocate()函数直接调用free函数释放内存,无须关心其他问题,重点在于内存不足情况下allocate()函数和reallocate()函数是如何应对的。
        allocate()函数首先调用malloc函数获取内存,在内存不足的情况下,该函数会返回空指针NULL,当malloc函数返回NULL时,则会调用oom_allocate()函数尝试释放一些内存并再次进行申请。
        这就是第一级空间配置器所提供的一种缓冲机制,第一级配置器中定义了一个函数指针,这个指针所指向的函数由用户所定义,因为只有用户知道哪些内存可以被释放来腾出空间,如果没有为该函数指针赋予相应的函数,则此时直接会抛出bad_alloc异常,若该函数指针被指定,则会不停调用该函数,直到申请到足够的内存,这里把它叫做内存不足处理函数,它的运行过程如图所示


第一级内存配置器allocate

        reallocate函数的内部运行过程和allocate函数的过程是相似的,只不过把malloc换成了realloc,oom_allocate换成了oom_reallocate,过程都是一样的。

第二级空间配置器

        第二级内存配置器负责小内存的管理
        当申请大量的小内存时,一方面会把完整的内存区间划分的很破碎,当再次申请较大的内存时,可能会出现没有足够长的区间的情况,另一方面,大量的小区间也会使操作系统用来记录内存状态的数据结构很臃肿。
        第二级内存配置器所采取的策略是,在第一次申请小内存时,先申请一大块内存留作备用,以后再申请小内存时,直接从上次申请的那一大块内存中划去要求的部分,不再向系统申请。
        同样的,第二级空间配置器提供了标准接口allocate()、deallocate()、reallocate()三个接口,在介绍这三个接口之前,先介绍一下接下来会遇到的一些名词。
        1. 内存区块,有时也简称区块
        内存区块是指一块小内存,它的大小均为8的倍数,最大为128Bytes,即有8、16、24、32、40、48、56、64、72、80、88、96、114、122、128这几种,内存区块有自己的首地址,可以存储数据。在每个区块的前8个字节,存储下一个可用区块的地址,通过这种方式,可以形成一条区块链表


内存区块,含有大小和地址两个要素

        2. freelist数组
        freelist数组是一个数组,内含16个元素,每一个元素是一个区块链表的首指针

        3. 内存池
        内存池是是一大块内存,它有三个参数:起始地址,终止地址以及大小,内存池的大小=终止地址 - 起始地址


内存池

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

运行过程

        当申请的内存大于128bytes时,直接转交第一级配置器进行内存申请。
        当申请的内存不大于128bytes时,假设申请n字节
        1. 计算(n + 7)/7,得到一个整数值i,这个i即为freelist的元素索引
        2. 访问freelist位于i的元素,此时该元素为NULL,不指向任何可用区块,这时将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函数

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这么多内存,同时更新内存池的状态
        如果内存池的剩余空间不够申请的那么多区块,即size < bytes_left < total_bytes,即能够供应一部分区块,则计算最多能划多少块,并划走

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

释放内存

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

内存区块链表

        freelist中,每一个元素都是obj*,obj的结构如图所示

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

        这里为什么要采用这种结构?
        首先考虑它的功能,它是内存区块的链表结点,它需要记录当前区块的地址,以及下个区块的地址,每个地址都是8个字节的指针,用一个struct来表示,需要16字节,而使用union结构,只需要8个字节
        在每个内存区块的前8个字节处,是个obj对象,它存储着下一个内存区块的地址,当用free_list_link来解引用这个指针时,有效区间为这8个字节,client_data是一个长度为1的数组,只有一个元素,它就是内存区块的第一个字节,为这个字节定义一个变量,并对它取址,得到的就是当前区块的地址,这里采用数组的形式而不是直接定义一个char,目的是直接将client_data作为数组首地址返回,而不需要调用取址运算符,将该内存区块返回时,返回client_data,无须进行类型转换,直接在union中切换就行,状态的改变不会改变前8个字节的内容,但内存区块交出去后,前八个字节的内容丢失也不重要了,在将内存区块加入到freelist中时,会重新设置前8个字节的值,保证数据的有效性。

上一篇下一篇

猜你喜欢

热点阅读