TCMalloc - 基本流程

SizeMap

tcmalloc通过classid将不同的小对象映射到不同的对象桶中,sizemap记录了一些对象大小和对象class的映射以及反向映射,除此之外,还记录了一些ThreadCache与CentralCache层交互的时候批量处理的一些数据。
class_to_size_[kClassSizesMax]数组记录每个class中存储的对象大小
class_to_pages_[kClassSizesMax]数组记录当centraol cache向page map申请内存时一次申请的page数量
num_objects_to_move_[kClassSizesMax]数组记录每个线程与central cache层申请和释放时批量处理的对象个数
 

ThreadCache

顾名思义,每个线程一份内存Cache,线程在分配和释放内存的时候,首先从ThreadCache中分配和释放,没有锁争用开销,非常高效。
最重要的数据结构,就是FreeList      list_[kClassSizesMax],每个class一个桶,每个桶中是一个FreeList单链表,将所有该class的对象都链接起来。另外还有一个ThreadCache的prev和next指针,主要用来做数据统计用。
tcmalloc中所有的链表都是上一个对象的头部4字节或8字节中存储下一个对象的地址,如下所示:

 

CentralCache

CentralCache是所有线程共同使用的公共小对象池。CentralCache由一些CentralFreeList组成,每个CentralFreeList管理一个class的小对象,线程在访问CentralCache的时候需要加锁,锁的粒度是每个对象桶CentralFreeList。
CetralFreeList的核心数据结构是两个Span链表,一个叫empty_,另一个叫nonempty_。empty_链表中是其所有object都已经分配完毕的span,nonempty_链表中是部分分配或未分配object的span。由于ThreadCache与CentralCache每次批发的objects数量是恒定的(由上文中提到的num_objects_to_move_来确定),而对span的操作相对来说比较耗时,所以CentralFreeList中加入了名叫tc_slots_的cache,将其作为span的一个cache,每次分配内存和回收内存的时候,如果分配和回收的内存刚好是num_objects_to_move_中相应的数量,则优先走tc_slots_。每次分配的时候,优先从tc_slots_中寻找特定数量的objects,每次回收内存的时候,则优先将objects回收到tc_slots_中。
 

PageHeap

PageHeap是page级别的allocator,它的主要职责就是管理page,上文中提到的span,就是若干连续page组成的数据结构。
PageHeap的核心数据结构有:
1)名为pagemap_的PageMap,它是用来映射Page地址PageID和Span的。这是一个三层的radix_tree,提供的最主要的接口有两个,一是名为set的设置PageID和Span的映射,另一个是名为get的通过PageID获取Span。
2)名为free_[kMaxPages]的一个SpanList数据,kMaxPages在4k页的系统中是255,所以free_中有1个Page到255个Page的所有规格,span可以按照任意大小进行拆分和组合。
3)名为large_的SpanList,它用来存放大于kMaxPages的超大Page。
空闲span的伙伴系统为上层提供span的分配与回收。当需要的span没有空闲时,可以把更大尺寸的span拆小(如果大的span都没有了,则需要重新找kernel分配);当span回收时,又需要判断相邻的span是否空闲,以便将它们组合。判断相邻span还是要用到radix tree,radix tree就像一个大数组,很容易取到当前span前后相邻的span。
在向tcmalloc申请n个page的Span的时候,优先从free_数组的第n个下标开始寻找,如果free_[n]链表非空,就从这个链表中摘取一个元素,否则,从第n+1个下标开始寻找第一个空闲span,找到后将它切分成n个Page和K个Page,n个Page返回给应用,K个Page插入到free_[k]链表中,如果依然找不到空闲Span,就向操作系统申请一大块内存再分配。
 
每个SpanList由两个Span链表组成,一个是normal,一个是returned。normal链表是正常的空闲链表,returned链表是将要归还给操作系统的Span组成的空闲链表。在使用MallocExtension::GetStats打印出来tcmalloc的内存占用信息中,free_bytes统计来自normal链表,unmapped_bytes统计来自returned链表。
 
 

内存的分配过程

根据请求size判断是大块内存还是小块内存(256KB为边界,这个信息通过查询SizeMap表获得)
1,小块内存
     1), 通过size从SizeMap表中查到改请求对应的classid
     2), 从当前线程所在的ThreadCache的list_[classid]空闲链表中分配,如果分配成功则返回
     3), 尝试从CentralCache.list[class]空闲链表中一次性批发一批空闲object,返回一个给用户,其余的加入到ThreadCache.list_[class]空闲链表中。
     CentralFreeList中申请一批空闲object的申请顺序是:
     1) 优先从tc_slots中获取该数量的objects,tc_slots是一批空闲object的Cache,获取到则返回
     2) 从nonempty_链表中获取一个span,从该span中截取该数量的一批Objects,如果截取完毕之后span内的objects都用完了,那么将这个span插入到empty_链表中,否则增加span的引用计数,增加的数量是object的数量,获取到足够数量则返回
     3) 从nonempty_链表中尝试获取足量objects
     4) 向PageHeap申请若干个Page作为一个span,然后从span中截取足量的Objects,将截取完毕之后的span加入到nonempty_链表中
     PageHeap中申请n个Page的顺序是:
     1) 从PageHeap的free_[n]开始寻找空闲Span,首先尝试从free_[n].normal链表中寻找一个空闲Span,找到后从这个Span中截取n个Page返回,剩下的Page放入到对应的free_[x]链表中; 如果normal中获取不到,那么从free_[n].returned链表中寻找一个空闲的Span,然后做切分的操作。free[n]中找不到,从free[n+1]开始找,直到free[kMaxPages]
     2) 从large_链表中寻找,依然是先normal链表,再returned链表,找到后做切分的操作
     3) 尝试从操作系统申请一块至少kMaxPages大小的内存,继续1) 2)中的步骤
2,大块内存
     大块内存首先计算出需要多少个Page,然后从PageHeap中申请n个Page,流程同上。
 

内存的回收流程

    1,通过ptr的地址找到它所在的Page。对于4k Page的系统来说,每个地址除以4k就是它对应的PageId。
     2,通过PageId在pageheap中找到它对应的Span数据结构,从Span中获取到object所属的class。
    3,如果上面的Span中记录的class是0,说明这是一块大内存,直接调用PageHeap.Delete(Span*)来释放这块大内存;否则是一块小内存,小内存的回收:
        3.1 将这块内存插入到ThreadCache.freeList[cl]链表中,如果发现插入后的总长度大于了max_length,则尝试将若干个objects集体回收到centralCache中。
        3.2 尝试将这些Objects放入centralCache[cl]的FreeList中,如果发现centrailCache[cl]的FreeList已经有足够多的元素,则将这些objects集体回收到span中(通过调用ReleaseListToSpans(start)实现)。
        3.3 通过start地址找到pageId,然后在pageheap中通过PageId找到这些objects所属的span,将这些对象都插入到span->objects列表中。这一步中如果发现这个span上的objects都没有使用者了,就调用PageHeap.Delete(Span*)来释放这个Span。
 
     4, 释放Span:
        4.1 将这个span尝试放入到normal_freelist中。这个过程中,会尝试将pageheap中与此span内存地址相邻的span做合并,之后将合并后的span插入到free_[span->length]链表(小于128个page)或large_链表(大于128个page)中
        4.2 调用PageHeap::IncrementalScavenge(span->length)尝试去归还一部分Span给系统,并将这个Span插入到free_[span->length]链表或者large_链表的returned队列中。
        TCMalloc调用内存释放的接口TCMalloc_SystemRelease,它对应的系统调用的接口是madvise(),建议系统的行为是MADV_FREE,而MADV_FREE则将这些页标识为延迟回收。当内核内存紧张时,这些页将会被优先回收,如果应用程序在页回收后又再次访问,内核将会返回一个新的并设置为0的页。而如果内核内存充裕时,标识为MADV_FREE的页会仍然存在,后续的访问会清掉延迟释放的标志位并正常读取原来的数据,因此应用程序不检查页的数据,就无法知道页的数据是否已经被丢弃。
      因为 Linux 不支持 MADV_FREE,所以使用了 MADV_DONTNEED。使用 MADV_DONTNEED调用 madvise,告诉内核这段内存今后"很可能"用不到了,其映射的物理内存尽管拿去好了,因此,TCMalloc_SystemRelease 只是告诉内核,物理内存可以回收以做它用,但虚拟空间还留着,下次访问时会产生缺页中断,这个缺页中断会触发重新申请物理内存的操作。
      因此Span returned队列中的内存还是可以重新被上层模块申请使用的。
posted @ 2019-05-12 23:07  CobbLiu  阅读(1197)  评论(0编辑  收藏  举报