jemalloc中提供的malloc函数叫做je_malloc, 释放的函数是je_free.
jemalloc基础知识
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等size_index: size 位于size_class中的索引号,区间为 [0,231]?,比如8字节则为0,14字节(按16计算)为1,4kb字节为28,当 size 是small_class时,size_index也称作binind
base
1 | struct base_s { |
用于分配 jemalloc 元数据内存的结构
base.blocks.extent: 存放每个size_class的extent元数据- base.blocks是一个链表, 通过其next可以找到当前
ind维护的所有的extent元数据信息
bin
1 | typedef struct bin_s bin_t; |
管理正在使用中的 slab(即用于小内存分配的 extent) 的集合,每个 bin 对应一个 size_class
bin.slabcur: 当前使用中的slabbin.slabs_nonfull: 有空闲内存块的slab
bin_infos[binind]
1 | typedef struct bin_info_s bin_info_t; |
1 | typedef struct bitmap_info_s { |
管理相同binind 的所有bin的信息, 注意不是维护bin的链表, 只是存bin所属的相关sizeclass的相关信息, 从bitmap_info中获取regions组的信息(在使用TREE的情况下, 获取TREE深度信息)
extent
管理 jemalloc 内存块(即用于用户分配的内存)的结构,每一个内存块大小可以是 N * page_size(4kb)(N >= 1)。每个 extent 有一个序列号(serial number)。
一个 extent 可以用来分配一次 large_class 的内存申请,但可以用来分配多次 small_class 的内存申请。
extent.e_bits: 8字节长,记录多种信息extent.e_addr: 管理的内存块的起始地址extent.e_slab_data: 位图bitmap,当此extent用于分配small_class内存时,用来记录这个extent的分配情况,此时每个extent内的小内存称为region, 和bin_infos中的bitmap_info 配合使用, 每个region是否被使用占一个bit, 一个bitmap在64位上可以管理64个region. 查使用遍历每个bitmap, 64位上没有使用TREE结构, 即查看bitmap内从右边开始第一个非0的bit位, 在该bit位前的一个位置(未使用的), 是可以使用的region.
slab
当 extent 用于分配 small_class 内存时,称其为 slab。一个 extent 可以被用来处理多个同一 size_class 的内存申请。
extents
管理 extent 的集合。
extents.heaps[NPSIZES+1]: 各种page(4kb)倍数大小的extentextents.lru: 存放所有extent的双向链表extents.delay_coalesce: 是否延迟extent的合并
arena
1 | struct arena_s { |
挂到arenas[ind]下, struct arena_s
用于分配&回收 extent 的结构,每个用户线程会被绑定到一个 arena 上,默认每个逻辑 CPU 会有 4 个 arena 来减少锁的竞争,各个 arena 所管理的内存相互独立。
arena.extents_dirty: 刚被释放后空闲extent位于的地方arena.extents_muzzy:extents_dirty进行 lazy purge 后位于的地方,dirty -> muzzyarena.extents_retained:extents_muzzy进行 decommit 或 force purge 后extent位于的地方,muzzy -> retainedarena.large: 存放large extent的extentsarena.extent_avail: heap,存放可用的extent元数据arena.bins[NBINS]: 所以用于分配小内存的binarena.base: 用于分配元数据的base, base中有base_blocks管理所有当前ind的blocks链表, 而arena由areans[ind] 管理
rtree
1 | typedef struct rtree_ctx_s rtree_ctx_t; |
全局唯一的存放每个 extent 信息的 Radix Tree,以 extent->e_addr 即 uintptr_t 为 key,如uintptr_t 为64位(8字节), rtree 的高度为2
cache_bin
1 | typedef struct cache_bin_s cache_bin_t; |
每个线程独有的用于分配小内存的缓存
low_water: 上一次 gc 后剩余的缓存数量cache_bin.ncached: 当前cache_bin存放的缓存数量cache_bin.avail: 可直接用于分配的内存,从左往右依次分配(注意这里的寻址方式)
tcache
1 | struct tcache_s { |
每个线程独有的缓存(Thread Cache),大多数内存申请都可以在 tcache 中直接得到,从而避免加锁
tcache.bins_small[NBINS]: 小内存的cache_bintcache.bins_large[NSIZES - NBINS]: 大内存的cache_bin
tsd
1 | struct tsd_s { |
Thread Specific Data,每个线程独有,用于存放与这个线程相关的结构
tsd.rtree_ctx: 当前线程的 rtree context,用于快速访问extent信息tsd.arena: 当前线程绑定的arenatsd.tcache: 当前线程的tcache
je初始化及内存申请
进行内存申请时, 需要先进行初始化
1.1 初始化参数
1 | --> 1.1 初始化参数 |
1.2 extent默认的处理函数
1 | --> 1.2 extent默认的处理函数, 没贴的函数就是没打开宏的 |
1.3 SIZE_CLASSES定义
1 | --> 1.3 宏展开 |
1.4 arena_boot 展开
1 | void |
1.5 高级宏展开
MALLOC_TSD tsds 结构体
1 | /* O(name, type, nullable type */ |
相关的函数
1 | rb_proto(, extent_avail_, extent_tree_t, extent_t) |
e_bits相关
1 | EXTENT_BITS_ARENA_WIDTH = 12 ; |
je_malloc 基本流程
1 | -> je_malloc(size_t size) |
Small类型内存分配分为两类,一类是arena_malloc_hard,另一类是arena_malloc_small。
这两类的区别是tcache_alloc_small申请的内存有和线程进行绑定,即申请的内存数据会设置为线程私有数据(tsd),而arena_malloc_hard申请的内存没有和线程绑定。
在imalloc_body流程中, 会获取tcache tcache = tcaches_get(tsd, dopts->tcache_ind), 而在初始化或者slow malloc时, tcache是NULL, 就需要从arena->bins中获取.
tcache中获取, 先从
tcache->bins_small[binind]取出cache_bin的信息, 通过cache_bin_alloc_easy函数取出*(bin->avail - bin->ncached)返回出去, 这个即为对应的可用的region, 此处bin->ncached保存的可用的缓存数目, 如果bin->ncached为0, 则需要重新装填bins_small结构, [见1.7 tsd->tcache->bins_small 填充流程](#1.7 tsd->tcache->bins_small 填充流程), 装填完后, 再次通过cache_bin_alloc_easy函数取出*(bin->avail - bin->ncached)返回出region不从tcache中获取, 需要加锁, 因为arena是全局的变量, 不是线程特有的. 通过
arena_malloc_hard函数, 从arena->bins[ind]取出bin, 此处的bin不是tcache的cache_bin结构, 而是开篇介绍的bin结构, 其中bin->slabcur是当前bin使用的extent结构, 需要从其位图中找出可用的region. 如果slabcur是空的, 或者当前的slab(extent)没有可用的region, 则需要调用arena_bin_nonfull_slab_get函数获取, 这里和[1.6 tsd->tcache->bins_small 填充流程](#1.6 tsd->tcache->bins_small 填充流程)流程是重复的, 需要理清tache->bins_small[ind]和arena->bins[ind]之间的关系.在装填过程中, 可以看出 arena->bins[ind] 维护的 bin 是一个全局的大缓存, 而tache的cache_bin是一个和tsd绑定的小缓存, 当小缓存ncached变成0时, 即没有可用的region后, 需要装填cache_bin. 优先级是先从大缓存中装入(先找slabcur, slabcur满了, 再找slab_nofull出堆, 找free的region, 同时更新slabcur), 如果大缓存没有了, 就需要从extents_dirty(muzzy|retained)(heap)中回收, 回收也没有的话, 就需要mmap然后新建slab, 再分给小缓存(也包括更新大缓存, 和把extent注册到rtree中)
1.6 tsd->tcache->bins_small 填充流程
“
lg_fill_div用作tcache refill时作为除数. 当tcache耗尽时, 会请求arena bins_small进行refill. 但refill不会一次性灌满tcache, 而是依照其最大容量缩小2^lg_fill_div的倍数. 该数值同low_water一样是动态的, 两者互相配合确保tcache处于一个合理的充满度”
1 | --> arena_tcache_fill_small(tsdn_t *tsdn, arena_t *arena, tcache_t *tcache, cache_bin_t *tbin, szind_t binind, uint64_t prof_accumbytes) |
首先从 tsd->tcache->bins_small[binind] 中获取对应 size_class 的内存,有的话将内存直接返回给用户,如果 bins_small[binind] 中没有的话,需要通过 slab(extent) 对 tsd->tcache->bins_small[binind] 进行填充,一次填充多个以备后续分配,填充方式如下(当前步骤无法成功则进行下一步):
- 通过
bin->slabcur分配 - 从
bin->slabs_nonfull中获取可使用的extent - 从
arena->extents_dirty中回收extent,回收方式为 *first-fit*,即满足大小要求且序列号最小地址最低(最旧)的extent,在arena->extents_dirty->bitmap中找到满足大小要求并且第一个非空 heap 的索引i,然后从extents->heaps[i]中获取第一个extent。由于extent可能较大,为了防止产生内存碎片,需要对extent进行分裂(伙伴算法),然后将分裂后不使用的extent放回extents_dirty中 - 从
arena->extents_muzzy中回收extent,回收方式为 *first-fit*,即满足大小要求且序列号最小地址最低(最旧)的extent,遍历每个满足大小要求并且非空的arena->extents_dirty->bitmap,获取其对应extents->heaps中第一个extent,然后进行比较,找到最旧的extent,之后仍然需要分裂 - 从
arena->extents_retained中回收extent,回收方式与extents_muzzy相同 - 尝试通过
mmap向内核获取所需的extent内存,并且在rtree中注册新extent的信息 - 再次尝试从
bin->slabs_nonfull中获取可使用的extent
简单来说,这个流程是这样的,cache_bin -> slab -> slabs_nonfull -> extents_dirty -> extents_muzzy -> extents_retained -> kernel
1.7 extent_alloc流程
1 | --> extent_t * extent_alloc(tsdn_t *tsdn, arena_t *arena) |
在small和large分配的函数中, 牵扯到的比较重要的函数就是这个extent_recycle了
下面对其重点分析一下
extent_recycle 回收函数分析
Tries to satisfy the given allocation request by reusing one of the extents in the given extents_t.
1 | static extent_t * |
1 | --> extent_recycle(tsdn_t *tsdn, arena_t *arena, extent_hooks_t **r_extent_hooks, |
je_free 流程
上面大概只分析了small 分配的流程, 还需要深入调研下,留待后续进行研究。
先来看下 free的流程, 加深对extent_recycle过程的理解
1 | --> je_free(void *ptr) |
上述流程中涉及到了 arena_extents_dirty_dalloc方法是内存被释放后, 与dirty extents堆的交互过程, 重点看下这部分, 其他都是相关bin 数据结构的关系整理.
arena_extents_dirty_dalloc extent变成了所有 region全是未使用的状态时的处理
1 | # "输入为extent, 即上面传下来的slab, 还有arena, 触发条件是extent变成了所有 region全是未使用的状态" 函数执行前后需要加锁, 略过 |
小内存释放小结
总结上述小内存释放的流程, 当前仅关注fast场景
根据调用上下文执行流程, 找到其绑定的tsd
根据tsd找到其tcache成员
fast场景, 根据全局rtree, ptr地址为key, 检索出其size ind, 以及是否是 slab (地址是否是小内存的)
如果没有tcache, 走
arena_dalloc_no_tcache(tsdn, ptr)流程, 一般情况下都是有tcache的, 走arena_dalloc_small(tsdn, ptr)流程由
iealloc(tsdn, ptr)根据全局rtree, ptr地址为key, 检索出其 extent 结构, 由extentextent_arena_get(extent)获取extent对应的arena, 由extentextent_szind_get(extent)获取binind, 进而&arena->bins[binind]获得bin结构,arena_slab_regind(slab, binind, ptr)获得ptr在extent中对应的region id. 清除该regionid 在extent中的使用标记,extent_nfree_inc(slab)extent 元信息中记录其 free region数+1如果释放后, 该extent变成了全free 状态(即所有region 都是free的), arena active page数 - 该extent所占的page数, 最后将该extent或者合并后的extent挂入
dirty堆中, 这时, 如果支持合并的话(have_dss对应的宏打开), 查extent前后的extent, 循环合并, 直到合并不了为止6.1 ptr所在的slab是 bin->slabcur, 将slabcur 设成NULL
6.2 ptr所在slab不是slabcur的情况下, 如果其bin_info->nregs == 1, 即释放前bin中的nregs(总region数)是1, 释放后, 该bin就变成未使用的状态了, 需要将其从slabfull 列中删除, 如果不是1(肯定也不是0, 就是>1的情况), 将其从nonfull堆中删除
bin nregs 是所有ind extent的region的总数
如果释放后, nfree释放后恰好变成了1, 即从full状态变成了nonfull状态. 将
arena->bins[binind]->slabcur切换为这个extent,前提是这个extent“更旧”(序列号更小地址更低),并且将替换后的extent移入arena->bins[binind]->slabs_nonfull, 并从slab_full队中删除.
extent_try_coalesce 合并extent
1 | static extent_t * |
去除多线程的逻辑外, 大致的流程是
1 | -->extent_t * extent_try_coalesce(tsdn_t *tsdn, arena_t *arena, extent_hooks_t **r_extent_hooks, rtree_ctx_t *rtree_ctx, |
extent_coalesce 合并extent
看起来没有实现, 没有配置合并, 如果配置了, 会走
extent_dss_mergeable(extent a, extent b)流程
1 | --> bool extent_coalesce(tsdn_t *tsdn, arena_t *arena, extent_hooks_t **r_extent_hooks, |