看完就不慌了!Netty源码之SizeClasses,秒会

概述

接下来的是详解 Netty 基于 jemalloc4 重构内存分配的思想以及源码。jemalloc4 相较于 jemalloc3 最大的提升是进一步优化内存碎片问题,因为在 jemalloc3 中最多可能会导致 50% 内存碎片,但 jemalloc4 通过划分更细粒度的内存规格在一定程度上改善了这一问题,这也是SizeClasses的由来。

Netty 重构了和内存分配相关的核心类,比如 PoolArena、PoolChunk、PoolSubpage 以及和缓存相关的 PoolThreadCache,并且新增了一个 SizeClasses 类。从整体上看,Netty 分配内存的逻辑是和 jemalloc3 大致相同:

  1. 首先尝试从本地缓存中分配,分配成功则返回。
  2. 分配失败则委托 PoolArena 进行内存分配,PoolArena 最终还是委托 PoolChunk 进行内存分配。
  3. PoolChunk 根据内存规格采取不同的分配策略。
  4. 内存回收时也是先通过本地线程缓存回收,如果实在回收不了或超出阈值,会交给关联的 PoolChunk 进行内存块回收。

jemalloc4 主要是对 PoolChunk 的内存分析进行了重构,这是我们这两篇文章分析的重点类。但是在分析它之前我们还需要对 SizeClasses 这个规格类进行讲解。 在旧版本中,对内存规格是按下图划分的:

[图片上传失败...(image-724588-1612361221890)]

仔细发现,在 Small 级别的内存分配中会存在大量的内存碎片: 比如用户申请内存大小为 1025,按 jemalloc3 算法会向 PoolChunk 申请 2048Byte 的内存块,这将会导致 50% 内存碎片。那我们看看 jemalloc4 是如何解决的。

看完就不慌了!Netty源码之SizeClasses,秒会

从上图可以看出,jemalloc4 返回的规格值为 1280,因此大大减少内存碎片。也可以看出,jemalloc4 取消了 Tiny 级别,如今只有 Small、Normal 和 Huge,而 SizeClasses 就是记录 Small 和 Normal 规格值得一张表(table),这张表记录了很多有用的信息。

SizeClasses

这是一个极其重要类,它在内部维护一个二维数组,这个数组存储与内存规格有关的详细信息。我们先看看这张表长什么样子的:

看完就不慌了!Netty源码之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:

[图片上传失败...(image-873c44-1612361221889)]

从表中可以发现,不管对于哪种内存规格,它都有更细粒度的内存大小的划分。比如在 512Byte~8192Byte 范围内,现在可分为 512、640、768 等等,不再是 jemalloc3 只有 512、1024、2048 ... 这种粒度比较大的规格值了。这就是 jemalloc4 最大的提升。

size = (1 << log2Group) + nDelta * (1 << log2Delta)

我们可以简单研究一下这个公式,这个公式就是通过 SizeClasses 记录的信息计算对应的 size 大小。至于如何得到这一串的公式我觉得不必深究,只需要这样做是为了更细粒度拆分内存块,以免减少内存碎片。

看完就不慌了!Netty源码之SizeClasses,秒会

SizeClasses 体系结构

我们可以把 SizeClasses 看成是一个数组结构,最重要是存储数组索引 index 和 size 的映射关系。当然,还维护了其他数组以避免多次计算。我们先对 SizeClasses 的结构有一个大致的了解,后面再了解重要的 API。

[图片上传失败...(image-da67cc-1612361221889)]

PoolArena 这个大管家通过继承 SizeClasses 拥有内部的数据结构,可以直接调用相关 API。接口 SizeClassMetric 定义了与 SizeClasses 相关的核心的 API。

SizeClassesMetric

看完就不慌了!Netty源码之SizeClasses,秒会

上面简单对 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 的索引值。
看完就不慌了!Netty源码之SizeClasses,秒会

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有帮助,喜欢的小伙伴可以帮忙转发+ 关注,感谢大家!

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容