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
: 当前使用中的slab
bin.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)
倍数大小的extent
extents.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 -> muzzy
arena.extents_retained
:extents_muzzy
进行 decommit 或 force purge 后extent
位于的地方,muzzy -> retained
arena.large
: 存放large extent
的extents
arena.extent_avail
: heap,存放可用的extent
元数据arena.bins[NBINS]
: 所以用于分配小内存的bin
arena.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_bin
tcache.bins_large[NSIZES - NBINS]
: 大内存的cache_bin
tsd
1 | struct tsd_s { |
Thread Specific Data,每个线程独有,用于存放与这个线程相关的结构
tsd.rtree_ctx
: 当前线程的 rtree context,用于快速访问extent
信息tsd.arena
: 当前线程绑定的arena
tsd.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, |