1. Jemalloc简介
jemalloc 是由 Jason Evans 在 FreeBSD 项目中引入的新一代内存分配器。它是一个通用的 malloc 实现,侧重于减少内存碎片和提升高并发场景下内存的分配效率,其目标是能够替代 malloc。jemalloc 在 2005 年首次作为 FreeBSD libc 分配器使用,2010年,jemalloc 的功能延伸到如堆分析和监控/调优等。现代的 jemalloc 版本依然集成在 FreeBSD 中。
jemalloc 应用十分广泛,在 Firefox、Redis、Rust、Netty 等出名的产品或者编程语言中都有大量使用。具体细节可以参考 Jason Evans 发表的论文 《A Scalable Concurrent malloc Implementation for FreeBSD》。这篇文章介绍 JeMalloc-5.1.0 版本。
2. 基础知识
下图是旧版本的jemalloc:
旧版本的jemalloc中, 每个arena内都会包含对应的管理信息,记录该arena的分配情况。arena都有专属的chunks, 每个chunk的头部都记录了chunk的分配信息。在使用某一个chunk的时候,会把它分割成很多个run,并记录到bin中。不同size的class对应着不同的bin,在bin里,都会有一个红黑树来维护空闲的run,并且在run里,使用了bitmap来记录了分配状态。此外,每个arena里面维护一组按地址排列的可获得的run的红黑树。
对于jemalloc 5.1.0版本来说,有几点需要说明:
- chunk 这一概念被替换成了 extent;
- dirty page 的 decay(或者说 gc) 变成了两阶段,dirty -> muzzy -> retained;
- huge class 这一概念不再存在;
- 红黑树不再使用,取而代之的是 pairing heap;
2.1 size_class
每个 size_class
代表 jemalloc 分配的内存大小,共有 NSIZES(232)个小类(如果用户申请的大小位于两个小类之间,会取较大的,比如申请14字节,位于8和16字节之间,按16字节分配),分为2大类:
small_class
(小内存) :
对于64位机器来说,通常区间是 [8, 14kb],常见的有 8, 16, 32, 48, 64, …, 2kb, 4kb, 8kb,注意为了减少内存碎片并不都是2的次幂,比如如果没有48字节,那当申请33字节时,分配64字节显然会造成约50%的内存碎片
large_class
(大内存):
对于64位机器来说,通常区间是 [16kB, 7EiB],从 4 * page_size 开始,常见的比如 16kB, 32kB, …, 1mB, 2mB, 4mB,最大是 $2^{62}+3^{60}$,对于64位操作系统,页大小为4kB。
size_index
(size 索引):
size 位于 size_class
中的索引号,区间为 [0,231],比如8字节则为0,14字节(按16计算)为1,4kb字节为28,当 size 是 small_class
时,size_index
也称作 binind
2.2 arena
arena 是 jemalloc 最重要的部分,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena,各个 arena 所管理的内存相互独立。
struct arena_s {
atomic_u_t nthreads[2];
tsdn_t *last_thd;
arena_stats_t stats; // arena的状态
ql_head(tcache_t) tcache_ql;
ql_head(cache_bin_array_descriptor_t) cache_bin_array_descriptor_ql;
malloc_mutex_t tcache_ql_mtx;
prof_accum_t prof_accum;
uint64_t prof_accumbytes;
atomic_zu_t offset_state;
atomic_zu_t extent_sn_next; // extent的序列号生成器状态
atomic_u_t dss_prec;
atomic_zu_t nactive; // 激活的extents的page数量
extent_list_t large; // 存放 large extent 的 extents
malloc_mutex_t large_mtx; // large extent的锁
extents_t extents_dirty; // 刚被释放后空闲 extent 位于的地方
extents_t extents_muzzy; // extents_dirty 进行 lazy purge 后位于的地方,dirty -> muzzy
extents_t extents_retained; // extents_muzzy 进行 decommit 或 force purge 后 extent 位于的地方,muzzy -> retained
arena_decay_t decay_dirty; // dirty --> muzzy
arena_decay_t decay_muzzy; // muzzy --> retained
pszind_t extent_grow_next;
pszind_t retain_grow_limit;
malloc_mutex_t extent_grow_mtx;
extent_tree_t extent_avail; // heap,存放可用的 extent 元数据
malloc_mutex_t extent_avail_mtx; // extent_avail的锁
bin_t bins[NBINS]; // 所有用于分配小内存的 bin
base_t *base; // 用于分配元数据的 base
nstime_t create_time; // 创建时间
};
几种extent的状态如下:
内存状态 | 备注 |
---|---|
clean | 分配给用户或 tcache |
dirty | 用户调用 free 或 tcache 进行了 gc |
muzzy | extents_dirty 对 extent 进行 |