ptmalloc tcmalloc jemalloc

一、malloc申请内存过程

malloc() 并不是系统调用,也不是运算符,而是 C 库里的函数,用于动态分配内存。
malloc申请内存的时候,会有两种方式向操作系统申请堆内存:

  • 方式一:通过brk()系统调用从堆分配内存。
  • 方式二:通过mmap()系统调用在文件映射区域分配内存。

二、brk()系统调用

2.1 brk()的申请方式

一般如果用户分配的内存小于 128 KB,则通过 brk() 申请内存。而brk()的实现的方式很简单,就是通过 brk() 函数将堆顶指针向高地址移动,获得新的内存空间。如下图:

malloc通过brk()方式申请的内存,free释放内存的时候,并不会把内存归还给操作系统,而是缓存在malloc的内存池中,以便可以重复使用。

2.2 brk()系统调用的优缺点

优点:可以减少缺页异常的发生,提高内存访问效率。
缺点:由于申请的内存没有归还系统,频繁的内存分配和释放会造成内存碎片。brk()方式之所以会产生内存碎片,是由于brk通过移动堆顶的位置来分配内存,并且使用完不会立即归还系统,如果高地址的内存不释放,低地址的内存是得不到释放的。
为了解决使用brk()会出现内存碎片,在申请大块内存的时候才会使用mmap()方式,mmap()是以页为单位进行内存分配和管理的,释放后就直接归还系统了,不会出现这种小碎片的情况。

三、ptmalloc

ptmalloc 是基于 glibc 实现的内存分配器,它是一个标准实现,所以兼容性较好。

3.1 多线程支持

Allocate的内存分配器中,为了解决多线程锁争夺问题,分为主分配区main_area和非主分配区no_main_area。
 1. 主分配区和非主分配区形成一个环形链表进行管理。
 2. 每一个分配区利用互斥锁使线程对于该分配区的访问互斥。
 3. 每个进程只有一个主分配区,也可以允许有多个非主分配区。
 4. ptmalloc根据系统对分配区的争用动态增加分配区的大小,分配区的数量一旦增加,则不会减少。
 5. 主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块
 6. 申请小内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。
当一个线程需要使用malloc分配内存的时候,会先查看该线程的私有变量中是否已经存在一个分配区。若是存在。会尝试对其进行加锁操作。若是加锁成功,就在使用该分配区分配内存,若是失败,就会遍历循环链表中获取一个未加锁的分配区。若是整个链表中都没有未加锁的分配区,则malloc会开辟一个新的分配区,将其加入全局的循环链表并加锁,然后使用该分配区进行内存分配。当释放这块内存时,同样会先获取待释放内存块所在的分配区的锁。若是有其他线程正在使用该分配区,则必须等待其他线程释放该分配区互斥锁之后才能进行释放内存的操作。

3.2 chunk

ptmalloc中分配/释放的内存都被表示成chunk。chunk按结构可分为使用中和空闲两种类型。
使用中的chunk:

说明:
chunk指针:指向chunk的开始地址;mem指针:指向用户内存块的开始地址;
A:主分区/非主分区标识符,0:主分配区分配,1:非主分配区分配;
M:0:heap区域分配,1:mmap映射区域分配;
P:前一个chunk的状态, 0:前一个chunk为空闲(此时prev_size有效), 1:前一个chunk正在使用(此时prev_size无效)。ptmalloc 分配的第一个块总是将P置为1, 以防止程序引用不存在的chunk区域。
P主要用于内存块的合并操作。

空闲的chunk:

说明:
空闲的chunk只有AP状态,无M状态;
原本是用户数据区的地方存储了四个指针:
1)fd/bk:分别指向后/前一个空闲chunk。malloc通过这两个指针将大小相近的chunk连成一个双向链表;
2)fd_nextsize/bk_nextsize:存在于large bin中,用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的。

3.3 空闲链表bins

用户free掉的内存,ptmalloc并不会马上还给os,而是用空闲链表bins管理起来了,这样当下次malloc一块内存时,ptmalloc会直接从bins上寻找一块合适大小的内存块分配给用户使用。这样的好处可以避免频繁的系统调用,降低内存分配的开销。
ptmalloc按chunk的大小将bins分成四类:fast/unsorted/small/large bins。

  • fast bins:

unsorted/small/large bins的高速缓冲区,大约有10个定长队列。每个fast bin都存储着一条空闲chunk的单链表,增删chunk都发生在链表的前端。
当用户释放一块不大于max_fast(默认值64B)的chunk时,会默认会被放到fast bins上。当需要给用户分配的 chunk <= max_fast时,malloc首先会到fast bins上寻找是否有合适的chunk。

  • unsorted bins:
    bins 数组的第一个元素,用于加快分配速度。当用户释放的内存大于max_fast或fast bins合并后的chunk都会首先插入unsorted bin中;

  • small bins:
    小于512字节的chunk称为small chunk,而保存small chunks的bin被称为small bin。small bin存储在bins数组第2个至第65个位置,前后两个bin相差8个字节,同一个bin中的chunk大小相同。

  • large bins:
    大于等于512字节的chunk称为large chunk,而保存large chunks的bin被称为large bin。large bin位于small bins后面。每一个large bin包含了给定范围内的chunk,large bin内的chunk按先大小后最近使用时间递减排序。
    并不是所有chunk都按以上四种方式进行组织,以下两种情况例外:

  • Top chunk:
    分配区的顶部空闲内存,当所有的bins上都不能满足内存分配要求时,就会使用top chunk进行分配。
    若Top chunk的大小大于用户请求的内存大小时,则分割top chunk成两部分:User chunk(用户请求大小)和Remainder chunk(剩余大小)。其中Remainder chunk将成为新的Top chunk;
    当Top chunk的大小小于用户请求的内存大小时,则通过sbrk(main arena)或mmap(thread arena)系统调用来扩容Top chunk。

mmaped chunk:
当用户请求的内存非常大(大于分配阀值,默认128K)时,需被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存的时候会直接还给os。

3.4 内存分配

  1. 获取分配区的锁,防止多线程冲突。
  2. 计算出实际需要分配的内存的chunk实际大小。
  3. 判断chunk的大小,如果小于max_fast(64B),则尝试去fast bins上取适合的chunk,如果有则分配结束。否则,下一步;
  4. 判断chunk大小是否小于512B,如果是,则从small bins上去查找chunk,如果有合适的,则分配结束。否则下一步;
  5. ptmalloc首先会遍历fast bins中的chunk,将相邻的chunk进行合并,并链接到unsorted bin中然后遍历 unsorted bins。如果unsorted bins上只有一个chunk并且大于待分配的chunk,则进行切割,并且剩余的chunk继续扔回unsorted bins;如果unsorted bins上有大小和待分配chunk相等的,则返回,并从unsorted bins删除;如果unsorted bins中的某一chunk大小 属于small bins的范围,则放入small bins的头部;如果unsorted bins中的某一chunk大小 属于large bins的范围,则找到合适的位置放入。若未分配成功,转入下一步;
  6. 从large bins中查找找到合适的chunk之后,然后进行切割,一部分分配给用户,剩下的放入unsorted bin中。
  7. 如果搜索fast bins和bins都没有找到合适的chunk,那么就需要操作top chunk来进行分配了,当top chunk大小比用户所请求大小还大的时候,top chunk会分为两个部分:User chunk(用户请求大小)和Remainder chunk(剩余大小)。其中Remainder chunk成为新的top chunk。当top chunk大小小于用户所请求的大小时,top chunk就通过sbrk(main arena)或mmap(thread arena)系统调用来扩容。
  8. 到了这一步,说明 top chunk 也不能满足分配要求,所以,于是就有了两个选择: 如 果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用 mmap 来分配一个新的 sub-heap,增加 top chunk 大小;或者使用 mmap()来直接分配。在 这里,需要依靠 chunk 的大小来决定到底使用哪种方法。判断所需分配的 chunk 大小是否大于等于 mmap 分配阈值,如果是的话,则转下一步,调用 mmap 分配, 否则跳到第 10 步,增加 top chunk 的大小。
  9. 使用 mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。 然后将内存指针返回给用户。
  10. 判断是否为第一次调用 malloc,若是主分配区,则需要进行一次初始化工作,分配 一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。若已经初 始化过了,主分配区则调用 sbrk()增加 heap 空间,分主分配区则在 top chunk 中切 割出一个 chunk,使之满足分配需求,并将内存指针返回给用户。
    简而言之: 获取分配区(arena)并加锁–> fast bin –> unsorted bin –> small bin –> large bin –> top chunk –> 扩展堆

3.5 内存回收

  1. 获取分配区的锁,保证线程安全。
  2. 如果free的是空指针,则返回,什么都不做。
  3. 判断当前chunk是否是mmap映射区域映射的内存,如果是,则直接munmap()释放这块内存。前面的已使用chunk的数据结构中,我们可以看到有M来标识是否是mmap映射的内存。
  4. 判断chunk是否与top chunk相邻,如果相邻,则直接和top chunk合并(和top chunk相邻相当于和分配区中的空闲内存块相邻)。转到步骤8
  5. 如果chunk的大小大于max_fast(64b),则放入unsorted bin,并且检查是否有合并,有合并情况并且和top chunk相邻,则转到步骤8;没有合并情况则free。
  6. 如果chunk的大小小于 max_fast(64b),则直接放入fast bin,fast bin并没有改变chunk的状态。没有合并情况,则free;有合并情况,转到步骤7
  7. 在fast bin,如果当前chunk的下一个chunk也是空闲的,则将这两个chunk合并,放入unsorted bin上面。合并后的大小如果大于64B,会触发进行fast bins的合并操作,fast bins中的chunk将被遍历,并与相邻的空闲chunk进行合并,合并后的chunk会被放到unsorted bin中,fast bin会变为空。合并后的chunk和topchunk相邻,则会合并到topchunk中。转到步骤8
  8. 判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。free结束。

3.6 ptmalloc的注意事项

  1. 后分配的内存先释放,因为ptmalloc收缩内存是从top chunk开始,如果与top chunk相邻的chunk不能释放,top chunk以下的chunk都无法释放。
  2. Ptmalloc不适合用于管理长生命周期的内存,特别是持续不定期分配和释放长生命周期的内存,这将导致ptmalloc内存暴增。
  3. 不要关闭 ptmalloc 的 mmap 分配阈值动态调整机制,因为这种机制保证了短生命周期的 内存分配尽量从 ptmalloc 缓存的内存 chunk 中分配,更高效,浪费更少的内存。
  4. 多线程分阶段执行的程序不适合用ptmalloc,这种程序的内存更适合用内存池管理
  5. 尽量减少程序的线程数量和避免频繁分配/释放内存。频繁分配,会导致锁的竞争,最终导致非主分配区增加,内存碎片增高,并且性能降低。
  6. 防止内存泄露,ptmalloc对内存泄露是相当敏感的,根据它的内存收缩机制,如果与top chunk相邻的那个chunk没有回收,将导致top chunk一下很多的空闲内存都无法返回给操作系统。
  7. 防止程序分配过多的内存,或是由于glibc内存暴增,导致系统内存耗尽,程序因为OOM被系统杀掉。预估程序可以使用的最大物理内存的大小,配置系统的/proc/sys/vm/overcommit_memory ,/proc/sys/vm/overcommit_ratio,以及使用ulimit -v限制程序能使用的虚拟内存的大小,防止程序因OOM被杀死掉。

四、tcmalloc

tcmalloc 出身于 Google,全称是 thread-caching malloc,所以 tcmalloc 最大的特点是带有线程缓存,tcmalloc 非常出名,目前在 Chrome、Safari 等知名产品中都有所应有。对多线程友好,tcmalloc 为每个线程分配了一个局部缓存,对于小内存(256K以下)的分配,基本不用加锁,可以直接由线程局部缓存来完成;大内存的分配通过自旋锁实现;

减少内存碎片;内存只增不减,除非通过API:releaseFreeMemory强制释放。

4.1 多级缓存

PageHeap:

PageHeap由若干个链表和一个集合组成;
链表节点称为span,同一个链表中节点大小相同,为N*Page,N从1至128,1Page=4K;
超过128Page的,由集合管理;
为了平摊系统调用的开销,PageHeap每次向os申请若干个Page。

CenterCache:

由若干个链表组成;
同一个链表中节点大小相同,不同链表按8k、16k、32K、48K、……、256K分类;
CenterCache的内存来源于PageHeap;

ThreadCache:

由若干个链表组成;
同一个链表中节点大小相同,不同链表按8k、16k、32K、48K、……、256K分类;
ThreadCache的内存来源于CenterCache;
TheadCache为线程的私有线程池,不需要加锁;

4.1 小对象分配

  • tcmalloc为每个线程分配了一个线程本地ThreadCache,小内存从ThreadCache分配,此外还有个中央堆(CentralCache),ThreadCache不够用的时候,会从CentralCache中获取空间放到ThreadCache中。

  • 小对象(<=32K)从ThreadCache分配,大对象从CentralCache分配。大对象分配的空间都是4k页面对齐的,多个pages也能切割成多个小对象划分到ThreadCache中。

  • 小对象有将近170个不同的大小分类(class),每个class有个该大小内存块的FreeList单链表,分配的时候先找到best fit的class,然后无锁的获取该链表首元素返回。如果链表中无空间了,则到CentralCache中划分几个页面并切割成该class的大小,放入链表中。

4.2 CentralCache分配管理

  • 大对象(>32K)先4k对齐后,从CentralCache中分配。 CentralCache维护的PageHeap如下图所示, 数组中第256个元素是所有大于255个页面都挂到该链表中。

  • 当best fit的页面链表中没有空闲空间时,则一直往更大的页面空间则,如果所有256个链表遍历后依然没有成功分配。 则使用sbrk, mmap, /dev/mem从系统中分配。

  • tcmalloc PageHeap管理的连续的页面被称为span.
    如果span未分配, 则span是PageHeap中的一个链表元素
    如果span已经分配,它可能是返回给应用程序的大对象, 或者已经被切割成多小对象,该小对象的size-class会被记录在span中

  • 在32位系统中,使用一个中央数组(central array)映射了页面和span对应关系, 数组索引号是页面号,数组元素是页面所在的span。 在64位系统中,使用一个3-level radix tree记录了该映射关系。

4.3 内存回收

  • 当一个object free的时候,会根据地址对齐计算所在的页面号,然后通过central array找到对应的span。
  • 如果是小对象,span会告诉我们他的size class,然后把该对象插入当前线程的ThreadCache中。如果此时ThreadCache超过一个预算的值(默认2MB),则会使用垃圾回收机制把未使用的object从ThreadCache移动到CentralCache的central free lists中。
  • 如果是大对象,span会告诉我们对象锁在的页面号范围。 假设这个范围是[p,q], 先查找页面p-1和q+1所在的span,如果这些临近的span也是free的,则合并到[p,q]所在的span, 然后把这个span回收到PageHeap中。
  • CentralCache的central free lists类似ThreadCache的FreeList,不过它增加了一级结构,先根据size-class关联到spans的集合, 然后是对应span的object链表。如果span的链表中所有object已经free, 则span回收到PageHeap中。

4.4 tcmalloc优点

  • hreadCache会阶段性的回收内存到CentralCache里。 解决了ptmalloc2中arena之间不能迁移的问题。
  • Tcmalloc占用更少的额外空间。例如,分配N个8字节对象可能要使用大约8N * 1.01字节的空间。即,多用百分之一的空间。Ptmalloc2使用最少8字节描述一个chunk。
  • 更快。小对象几乎无锁,>32KB的对象从CentralCache中分配使用自旋锁。 并且>32KB对象都是页面对齐分配,多线程的时候应尽量避免频繁分配,否则也会造成自旋锁的竞争和页面对齐造成的浪费

五、jemalloc

jemalloc 借鉴了 tcmalloc 优秀的设计思路,所以在架构设计方面两者有很多相似之处,同样都包含 thread cache 的特性。但是 jemalloc 在设计上比 ptmalloc 和 tcmalloc 都要复杂,jemalloc 将内存分配粒度划分为 Small、Large二个分类,并记录了很多 meta 数据,所以在空间占用上要略多于 tcmalloc,不过在大内存分配的场景,jemalloc 的内存碎片要少于 tcmalloc。

5.1 jemalloc原理

  • 与tcmalloc类似,每个线程同样在<32KB的时候无锁使用线程本地cache。
  • Jemalloc在64bits系统上使用下面的size-class分类:
Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
Huge: [4 MiB, 8 MiB, 12 MiB, …]
  • small/large对象查找metadata需要常量时间, huge对象通过全局红黑树在对数时间内查找。
  • 虚拟内存被逻辑上分割成chunks(默认是4MB,1024个4k页),应用线程通过round-robin算法在第一次malloc的时候分配arena, 每个arena都是相互独立的,维护自己的chunks, chunk切割pages到small/large对象。free()的内存总是返回到所属的arena中,而不管是哪个线程调用free()。

六、对比

PTmalloc TCmalloc Jemalloc
内存组织 (1)内存分配单位为chunk;(2)小于64B的chunk放在fast bin中;(3)64 - 512B放在small bin中;(4)512B - 128 KB放large bin中;(5)大于128KB不进行缓存;(6)合并后的chunk放在unsorted bin中; (1)内存有三层缓存:PageHeap、CentralCache和ThreadCache;(2)0 - 256KB小对象放在中央缓存和线程缓存中,分为84个不同大小类别,中央缓存多个线程共享,线程级缓存每个线程私有;(3)256KB - 1MB的中对象和1MB以上大对象放在PageHeap,每个page大小为8KB (1)小类区间为[8B, 14kb],共232个小类,每个类的大小并不都是2的次幂;(2)大类区间为[16kB, 7EiB],page大小为4KB,从4 * page开始;(3)内存分配单位为extent,每个extent大小为N * 4KB,一个 extent 可以用来分配一次 large_class 的内存申请,但可以用来分配多次 small_class 的内存申请。
分配流程 fast bin —> small bins —> unsorted bin —> large bin —> top chunk —> 增加top chunk(sbrk/mmap) 或者 mmaped chunk; (1)小对象:ThreadCache —> CentralCache —> PageHeap —> 内核;(2)中对象和大对象:PageHeap —> 内核 (1)小内存:cache_bin -> slab -> slabs_nonfull -> extents_dirty -> extents_muzzy -> extents_retained -> 内核(2)大内存:extents_dirty -> extents_muzzy -> extents_retained -> 内核
多线程支持 没有线程级缓存,每个线程进行内存分配和释放时,需要对分配区进行加锁 每个线程拥有线程级缓存,当进行小对象分配和释放时,不用加锁处理 每个线程拥有线程级缓存tcache,进行小内存分配和释放时,不用加锁
优点 它是一个标准实现,所以兼容性较好 (1)在多线程场景下,小对象内存申请和释放是无锁的,效率很高,中对象和大对象申请使用自旋锁;(2)ThreadCache会阶段性的回收内存到CentralCache里,解决了ptmalloc2中分配区之间不能迁移的问题;(3)占用更少的额外空间。例如,分配N个8字节对象可能要使用大约8N * 1.01字节的空间,即,多用百分之一的空间; (1)采用多个arena来避免线程同步,多线程的分配是无锁的;(2)细粒度的锁,比如每一个bin以及每一个extents都有自己的锁,并发度更高;(3)使用了低地址优先的策略,来降低内存碎片化
缺点 (1)管理长周期内存时,会导致内存爆增,因为与top chunk 相邻的 chunk 不能释放,top chunk 以下的 chunk 都无法释放;(2)内存不能从一个分配区移动到另一个分配区, 就是说如果多线程使用内存不均衡,容易导致内存的浪费;(3)如果线程数量过多时,内存分配和释放时加锁的代价上升,导致效率低下;(4)每个chunk需要8B的额外空间,空间浪费大 1)对齐操作比PTmalloc多浪费一些内存,有点空间换时间;(2)如果多个线程频繁分配大对象,对自旋锁的竞争会很激烈; (1)arena之间的内存不可见,导致两个arena的内存出现大量交叉从而无法合并;(2)大概需要2%的额外开销,tcmalloc是1%;
适用场景 不适合多线程场景和需要申请长周期内存,只适合线程数较少和申请短周期内存的场景 适合多线程的场景 适合多线程的场景,多线程并发度更好

资料:

https://zhuanlan.zhihu.com/p/581863694
https://zhuanlan.zhihu.com/p/448293503
https://zhuanlan.zhihu.com/p/497509956?utm_id=0
https://blog.csdn.net/MOU_IT/article/details/118464093
http://www.360doc.com/content/22/0908/22/496343_1047180556.shtml
https://www.jianshu.com/p/3a3736cb5ae8

posted @ 2023-09-21 15:53  小海哥哥de  阅读(536)  评论(0编辑  收藏  举报