1. 内存管理目标

2. 三种内存管理的比较


1. 内存管理目标

    内存管理的目的是实现了malloc(),free()以及一组其它的函数,以提供动态内存管理的支持。分 配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序。

    为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存, 并通过某种算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内 存分配要求。也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当 响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间 中找不到的情况下才分配一块新的内存。

    由此可见,内存管理的核心目标主要有两点:

  • 高效的内存分配和回收,提升单线程或者多线程场景下的性能;
  • 减少内存碎片,包括内部碎片和外部碎片,提高内存的有效利用率;

2. 三种内存管理的比较

    ptmalloc 是基于 glibc 实现的内存分配器,它是一个标准实现,所以兼容性较好。pt 表示 per thread 的意思。当然 ptmalloc 确实在多线程的性能优化上下了很多功夫。由于过于考虑性能问题,多线程之间内存无法实现共享,只能每个线程都独立使用各自的内存,所以在内存开销上是有很大浪费的。

    tcmalloc 出身于 Google,全称是 thread-caching malloc,所以 tcmalloc 最大的特点是带有线程缓存,tcmalloc 非常出名,目前在 Chrome、Safari 等知名产品中都有所应有。tcmalloc 为每个线程分配了一个局部缓存,对于小对象的分配,可以直接由线程局部缓存来完成,对于大对象的分配场景,tcmalloc 尝试采用自旋锁来减少多线程的锁竞争问题。

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

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%;

适用场景

  不适合多线程场景和需要申请长周期内存,只适合线程数较少和申请短周期内存的场景

适合多线程的场景

适合多线程的场景,多线程并发度更好

性能对比图如下:

jemalloc 例程 jemalloc tcmalloc区别_jemalloc 例程