概述
接下来的是详解 Netty 基于 jemalloc4 重构内存分配的思想以及源码。jemalloc4 相较于 jemalloc3 最大的提升是进一步优化内存碎片问题,因为在 jemalloc3 中最多可能会导致 50% 内存碎片,但 jemalloc4 通过划分更细粒度的内存规格在一定程度上改善了这一问题,这也是SizeClasses的由来。
Netty 重构了和内存分配相关的核心类,比如 PoolArena、PoolChunk、PoolSubpage 以及和缓存相关的 PoolThreadCache,并且新增了一个 SizeClasses 类。从整体上看,Netty 分配内存的逻辑是和 jemalloc3 大致相同:
- 首先尝试从本地缓存中分配,分配成功则返回。
- 分配失败则委托 PoolArena 进行内存分配,PoolArena 最终还是委托 PoolChunk 进行内存分配。
- PoolChunk 根据内存规格采取不同的分配策略。
- 内存回收时也是先通过本地线程缓存回收,如果实在回收不了或超出阈值,会交给关联的 PoolChunk 进行内存块回收。
jemalloc4 主要是对 PoolChunk 的内存分析进行了重构,这是我们这两篇文章分析的重点类。但是在分析它之前我们还需要对 SizeClasses 这个规格类进行讲解。 在旧版本中,对内存规格是按下图划分的:
仔细发现,在 Small 级别的内存分配中会存在大量的内存碎片: 比如用户申请内存大小为 1025,按 jemalloc3 算法会向 PoolChunk 申请 2048Byte 的内存块,这将会导致 50% 内存碎片。那我们看看 jemalloc4 是如何解决的。
从上图可以看出,jemalloc4 返回的规格值为 1280,因此大大减少内存碎片。也可以看出,jemalloc4 取消了 Tiny 级别,如今只有 Small、Normal 和 Huge,而 SizeClasses 就是记录 Small 和 Normal 规格值得一张表(table),这张表记录了很多有用的信息。
SizeClasses
这是一个极其重要类,它在内部维护一个二维数组,这个数组存储与内存规格有关的详细信息。我们先看看这张表长什么样子的:
从上表中可知,数组长度 76。每一列表示的含义如下:
- index : 由 0 开始的自增序列号,表示每个 size 类型的索引。
- log2Group : 表示每个 size 它所对应的组。以每 4 行为一组,一共有 19 组。第 0 组比较特殊,它是单独初始化的。因此,我们应该从第 1 组开始,起始值为 6,每组的 log2Group 是在上一组的值 +1。
- log2Delta : 表示当前序号所对应的 size 和前一个序号所对应的 size 的差值得 log2 的值。比如 index=6 对应的 size = 112,index=7 对应的 size= 128,因此 index=7 的 log2Delta(7) = log2(128-112)=4。不知道你们有没有发现,其实log2Delta=log2Group-2 。
- nDelta : 表示组内增量的倍数。第 0 组也是比较特殊,nDelta 是从 0 开始 + 1。而其余组是从 1 开始 +1。
- isMultiPageSize : 表示当前 size 是否是 pageSize(默认值: 8192) 的整数倍。后续会把 isMultiPageSize=1 的行单独整理成一张表,你会发现有 40 个 isMultiPageSize=1 的行。
- isSubPage : 表示当前 size 是否为一个 subPage 类型,jemalloc4 会根据这个值采取不同的内存分配策略。
- log2DeltaLookup : 当 index<=27 时,其值和 log2Delta 相等,当index>27,其值为 0。但是在代码中没有看到具体用来做什么。
有了上面的信息并不够,因为最想到得到的是 index 与 size 的对应关系。 在 SizeClasses 表中,无论哪一行的 size 都是由 _size = (1 << log2Group) + nDelta * (1 << log2Delta)_ 公式计算得到。因此通过计算可得出每行的 size:
从表中可以发现,不管对于哪种内存规格,它都有更细粒度的内存大小的划分。比如在 512Byte~8192Byte 范围内,现在可分为 512、640、768 等等,不再是 jemalloc3 只有 512、1024、2048 ... 这种粒度比较大的规格值了。这就是 jemalloc4 最大的提升。
size = (1 << log2Group) + nDelta * (1 << log2Delta)
我们可以简单研究一下这个公式,这个公式就是通过 SizeClasses 记录的信息计算对应的 size 大小。至于如何得到这一串的公式我觉得不必深究,只需要这样做是为了更细粒度拆分内存块,以免减少内存碎片。
SizeClasses 体系结构
我们可以把 SizeClasses 看成是一个数组结构,最重要是存储数组索引 index 和 size 的映射关系。当然,还维护了其他数组以避免多次计算。我们先对 SizeClasses 的结构有一个大致的了解,后面再了解重要的 API。
PoolArena 这个大管家通过继承 SizeClasses 拥有内部的数据结构,可以直接调用相关 API。接口 SizeClassMetric 定义了与 SizeClasses 相关的核心的 API。
SizeClassesMetric
上面简单对 SizeClassesMetric 核心 API 做了简要的说明,相关源码这里就不进行说明了(其实我也不太懂,2333)。只需要知道Netty 通过 SizeClasses 类对内存的大小进行更细粒度的划分,从而减少内部碎片即可,后续 Netty 会通过 size 找到索引值 index,也可以通过 index 找到对应的 size。
isMultiPageSize=1
抽取 SizeClasses 中 isMultiPageSize=1 的所有行组成下面的表格。每列表示含义解释如下:
- index: 对应 SizeClasses 的 index 列。
- size: 规格值。
- num of page: 包含多少个 page。
- 对应 SizeClasses#pageIdx2SizeTab 的索引值。
SizeClasses#pageIdx2sizeTab
有很多同学对这个数组表示疑惑,这个数组可以用来做些什么? 这个数组用来加速 size<=lookupMaxSize(默认值: 4096) 的索引查找。也就是说,当我们需要通过 size 查找 SizeClasses 对应的数组索引时,如果此时 size<=lookupMaxSize 成立,那么经过计算得到相应结果 pageIdx2sizeTab 的索引值,然后获取存储在 pageIdx2sizeTab 的值就是对应 SizeClasses 的 index。 那如何计算得到相应的结果 pageIdx2sizeTab 的索引值呢? 是由 idx=(size-1)/16 求得。比如当 size=4096,由公式求得 idx=255,此时 pageIdx2sizeTab[255]=27,因此 size=4096 对应的 SizeClasses 索引值为 27。
总结
我并没有对 SizeClasses 的源码进行分析,主要是分析了也没有用,里面的代码比较晦涩,就算弄懂了也就那么一回事,我们只注意最终的表结构就 OK 了。
由于篇幅限制,Netty的学习就更新到这里了,希望可以对大家学习Netty有帮助,喜欢的小伙伴可以帮忙转发+ 关注,感谢大家!