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,一个 |
分配流程 | 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%; |
适用场景 | 不适合多线程场景和需要申请长周期内存,只适合线程数较少和申请短周期内存的场景 | 适合多线程的场景 | 适合多线程的场景,多线程并发度更好 |
性能对比图如下: