打算一边学习tcmalloc的源码一边写总结文章。先从转述TCMalloc的一篇官方文档开始(TCMalloc : Thread-Caching Malloc)。
TCMalloc比glibc 2.3 的malloc(是一个单独的库叫做ptmalloc2)和其他测试过的malloc都要快。TCMalloc致力于减少多线程对锁的竞争,在获取小的内存对象时候基本上不需要锁,在获取大的内存对象时候,TCMalloc使用了细粒度的高效率的spinlocks。ptmalloc2当然也有做减少线程间对锁进行竞争的努力,它每一个线程都有一个areans,彼此互不关联,这样显然有个问题就是一个线程释放的内存另外一个线程无法使用,只能新申请内存,如此下去,内存只会暴涨了。TCMalloc另一个优点是非常高效的空间利用率,例如在分配N个8 bytes的objects内存的时候,大约最多只会有8N *0.01 bytes(其实是8N / 7),后面源码分析会解释)的overhead,相比之下ptmalloc则是每一个object用了4bytes头,overhead总共达到了N*4 bytes。
TCMalloc给每一个线程也分配一个线程本地的cache。然后小对象的分配都是从这个cache中获取。而线程本地的cache又是按需从所有线程共享的一个central heap中获取的。周期的垃圾回收可以把部分内存从线程本地的cache挪回central heap,达到内存分配在多个线程中按需调度的目的。
TCMalloc对待大的内存(文章说大于32K,但其实看代码应该是大于256K的内存)的分配是直接从central heap中使用页层级的分配器(一页内存就是按照 4K 对齐的内存区域)分配的,也就是一个大的对象地址总是按页对齐横跨多个页。
在central heap中内存也是按页向系统请求分配的,而这些页内存通常会被裁剪成许多小对象,例如一页4K的内存能够被裁减成32个大小为128字节的objects。
每一个小对象对应了一个大约有60项的map中的一项。每一项代表了包含这个size的内存,程序中用了一个链表把该size的可用的object串起来,总的来说这个map就是线程本地cahche。map中的表项按照尺寸划分的,对于前面小一点的size是每8个字节增加一项,大一点的就是16字节,具体的哪一个size对应哪一项,程序中有注释如下:
//
// Examples:
// Size Expression Index
// -------------------------------------------------------
// 0 (0 + 7) / 8 0
// 1 (1 + 7) / 8 1
// …
// 1024 (1024 + 7) / 8 128
// 1025 (1025 + 127 + (120<<7)) / 128 129
// …
// 32768 (32768 + 127 + (120<<7)) / 128 376
线程本地的cache每一个size对应的Index中放置的是一个linklist,叫做free list,把可用的object内存串起来了。
分配小对象的过程是:(1)找到要分配的size对应cache表中的那一项;(2)在线程本地的cache中即size对应的free list中寻找可用内存;(2)假如free list不是空的就取出第一个object返回,这个过程不需要锁;
假如free list是空的,则进行以下步骤:(1)从对应size项的central free list中(这个是所有线程共享的)提取一些对象内存;(2)提取到的一些对象内存放到对应的线程本地free list项中;(3)从新提取的一些对象中获取一个对象作为返回值。
假如central free list中可用内存也是空的,则(1)从central page allocator中分配出一些页内存;(2)把这些页内存划分成一个一个object内存;(3)把这些新的objects挂在对应的central free list上;(4)参照之前,从其中移动一些内存到线程本地free list中。
大的内存分配是按页直接从central heap page获取的。central heap page也是一个free list数组,第n项代表数个n页内存串起来的链表。
分配k pages的内存就直接从数组的第k项的free list找可用内存,如果是空的就从下一项找,一直等到找到再返回,如果这样还是找不到,就要找系统去申请内存了(sbrk,mmap,。。。)。假如在一个大于k的free list中找到了空闲内存,多出来的内存要再插到数组合适的位置中去。
一个 Span 对象代表一些连续页内存,具体来说就是它在central heap page中是某一个free list中的节点,也是central free list某一个free list中的节点。在后者中,他还会把自己代表的页内存划分成对应size的一个一个objects,并用链表方式串起来,并等待线程本地cache去获取。
TCMalloc还有一个所有线程共享的数据结构就是一个map(文中叫做 central array),记录某一个页内存的页号具体对应哪一个span:
一个32位的系统共有4GB的虚拟地址空间,总共对应2^20 4K页,所以这个map最多占用4M的内存,看起来还可以接受。但是在64位的系统上就不行了,所以64位系统我们使用radix tree来代替数组实现这样的map。
当释放一个内存对象的时候,我们算出他所在页的页号,然后在central array中找到该页号对应的Span,Span可以获取对应的sizeclass,然后可以知道是小内存对象还是大内存对象。假如是小块内存对象,把其插入线程本地的合适的free list中,假如线程本地cache超过了一个指定的大小(默认是2MB),垃圾收集器这时候会启动起来把没有在使用的objects从线程本地cache挪动到central free lists中。
假如是大块的内存对象,Span同时可以获取这个object占据的连续页内存,比如是占据了 [p,q]页,我们也会判断第p-1,q+1页,假如他们是空闲的,可以和[p,q]页合并,然后插入到central page heap的合适free list中。
Central Free Lists是一个Free list的链表数组,代表着每一个szie类别当前可用的内存,free list的每一个节点是Span,Span内部则又是一些objects构成的链表。
内存在线程本地的Cache和Central Free List之间的移动是以这些object为单位的,从Central Free list申请内存,会从中移动适量个数的object到线程本地Cache,线程本地Cache返还内存时,也会把这些objects放到Central Free list原来所在的Span中去。
当Span每分配出一个object就增加一个引用计数,每被返还一个object就释放一个引用计数,在返还时如果引用技术为0,即没有地方在引用这个Span了,这时就会把Span所占用的页直接释放回heap page。
前面也已经提过,当一个线程本地的Cache其超过2MB后会被垃圾回收的。垃圾回收的这个阈值也会随着线程个数的增加而降低,因为这样可以减少应用程序对内存过度的使用。
垃圾回收时决定在一个free list中回收多少个内存对象是由每个size对应的free list的低水位线 L 控制的。L 记录了上次垃圾回收时这个free list的最小长度,每次回收 L/2 个内存对象。这种使用历史记录作为对未来预测的算法有比较好的效果,使得不同size的内存对象可以比较快速的在各个线程中交换使用。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章