概述

前面已对 Netty 使用 jemalloc3(jemalloc3、jemalloc4 指代 Netty 实现的 Java 版本,而非 C) 实现的内存分配的思路以及源码进行详解,接下来的这两篇是详解 Netty 基于 jemalloc4 重构内存分配的思想以及源码。jemalloc4 相较于 jemalloc3 最大的提升是进一步优化内存碎片问题,因为在 jemalloc3 中最多可能会导致 50% 内存碎片,但 jemalloc4 通过划分更细粒度的内存规格在一定程度上改善了这一问题,这也是 SizeClasses 的由来。相关问题可见 Netty issue 10267jemalloc changeLog
Netty 重构了和内存分配相关的核心类,比如 PoolArena、PoolChunk、PoolSubpage 以及和缓存相关的 PoolThreadCache,并且新增了一个 SizeClasses 类。从整体上看,Netty 分配内存的逻辑是和 jemalloc3 大致相同:

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

jemalloc4 主要是对 PoolChunk 的内存分析进行了重构,这是我们这两篇文章分析的重点类。但是在分析它之前我们还需要对 SizeClasses 这个规格类进行讲解。
在旧版本中,对内存规格是按下图划分的:
内存规格_new.png
仔细发现,在 Small 级别的内存分配中会存在大量的内存碎片: 比如用户申请内存大小为 1025,按 jemalloc3 算法会向 PoolChunk 申请 2048Byte 的内存块,这将会导致 50% 内存碎片。那我们看看 jemalloc4 是如何解决的。
Netty内存规格_new.png
从上图可以看出,jemalloc4 返回的规格值为 1280,因此大大减少内存碎片。也可以看出,jemalloc4 取消了 Tiny 级别,如今只有 Small、Normal 和 Huge,而 SizeClasses 就是记录 Small 和 Normal 规格值的一张表(table),这张表记录了很多有用的信息。

SizeClasses

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

index log2Group log2Delta nDelta isMultiPageSize isSubPage log2DeltaLookup
0 4 4 0 0 1 4
1 4 4 1 0 1 4
2 4 4 2 0 1 4
3 4 4 3 0 1 4
4 6 4 1 0 1 4
5 6 4 2 0 1 4
6 6 4 3 0 1 4
7 6 4 4 0 1 4
8 7 5 1 0 1 5
9 7 5 2 0 1 5
10 7 5 3 0 1 5
11 7 5 4 0 1 5
12 8 6 1 0 1 6
13 8 6 2 0 1 6
14 8 6 3 0 1 6
15 8 6 4 0 1 6
16 9 7 1 0 1 7
17 9 7 2 0 1 7
18 9 7 3 0 1 7
19 9 7 4 0 1 7
20 10 8 1 0 1 8
21 10 8 2 0 1 8
22 10 8 3 0 1 8
23 10 8 4 0 1 8
24 11 9 1 0 1 9
25 11 9 2 0 1 9
26 11 9 3 0 1 9
27 11 9 4 0 1 9
28 12 10 1 0 1 0
29 12 10 2 0 1 0
30 12 10 3 0 1 0
31 12 10 4 1 1 0
32 13 11 1 0 1 0
33 13 11 2 0 1 0
34 13 11 3 0 1 0
35 13 11 4 1 1 0
36 14 12 1 0 1 0
37 14 12 2 1 1 0
38 14 12 3 0 1 0
39 14 12 4 1 0 0
40 15 13 1 1 0 0
41 15 13 2 1 0 0
42 15 13 3 1 0 0
43 15 13 4 1 0 0
44 16 14 1 1 0 0
45 16 14 2 1 0 0
46 16 14 3 1 0 0
47 16 14 4 1 0 0
48 17 15 1 1 0 0
49 17 15 2 1 0 0
50 17 15 3 1 0 0
51 17 15 4 1 0 0
52 18 16 1 1 0 0
53 18 16 2 1 0 0
54 18 16 3 1 0 0
55 18 16 4 1 0 0
56 19 17 1 1 0 0
57 19 17 2 1 0 0
58 19 17 3 1 0 0
59 19 17 4 1 0 0
60 20 18 1 1 0 0
61 20 18 2 1 0 0
62 20 18 3 1 0 0
63 20 18 4 1 0 0
64 21 19 1 1 0 0
65 21 19 2 1 0 0
66 21 19 3 1 0 0
67 21 19 4 1 0 0
68 22 20 1 1 0 0
69 22 20 2 1 0 0
70 22 20 3 1 0 0
71 22 20 4 1 0 0
72 23 21 1 1 0 0
73 23 21 2 1 0 0
74 23 21 3 1 0 0
75 23 21 4 1 0 0

从上表中可知,数组长度 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:

index log2Group log2Delta nDelta isMultiPageSize isSubPage log2DeltaLookup size Unit: MB
0 4 4 0 0 1 4 16
1 4 4 1 0 1 4 32
2 4 4 2 0 1 4 48
3 4 4 3 0 1 4 64
4 6 4 1 0 1 4 80
5 6 4 2 0 1 4 96
6 6 4 3 0 1 4 112
7 6 4 4 0 1 4 128
8 7 5 1 0 1 5 160
9 7 5 2 0 1 5 192
10 7 5 3 0 1 5 224
11 7 5 4 0 1 5 256
12 8 6 1 0 1 6 320
13 8 6 2 0 1 6 384
14 8 6 3 0 1 6 448
15 8 6 4 0 1 6 512
16 9 7 1 0 1 7 640
17 9 7 2 0 1 7 768
18 9 7 3 0 1 7 896
19 9 7 4 0 1 7 1024
20 10 8 1 0 1 8 1280
21 10 8 2 0 1 8 1536
22 10 8 3 0 1 8 1792
23 10 8 4 0 1 8 2048
24 11 9 1 0 1 9 2560
25 11 9 2 0 1 9 3072
26 11 9 3 0 1 9 3584
27 11 9 4 0 1 9 4096
28 12 10 1 0 1 0 5120
29 12 10 2 0 1 0 6144
30 12 10 3 0 1 0 7168
31 12 10 4 1 1 0 8192 8KB
32 13 11 1 0 1 0 10240 10KB
33 13 11 2 0 1 0 12288 12KB
34 13 11 3 0 1 0 14336 14KB
35 13 11 4 1 1 0 16384 16KB
36 14 12 1 0 1 0 20480 20KB
37 14 12 2 1 1 0 24576 24KB
38 14 12 3 0 1 0 28672 28KB
39 14 12 4 1 0 0 32768 32KB
40 15 13 1 1 0 0 40960 40KB
41 15 13 2 1 0 0 49152 48KB
42 15 13 3 1 0 0 57344 56KB
43 15 13 4 1 0 0 65536 64KB
44 16 14 1 1 0 0 81920 80KB
45 16 14 2 1 0 0 98304 96KB
46 16 14 3 1 0 0 114688 112KB
47 16 14 4 1 0 0 131072 128KB
48 17 15 1 1 0 0 163840 160KB
49 17 15 2 1 0 0 196608 192KB
50 17 15 3 1 0 0 229376 224KB
51 17 15 4 1 0 0 262144 256KB
52 18 16 1 1 0 0 327680 320KB
53 18 16 2 1 0 0 393216 384KB
54 18 16 3 1 0 0 458752 448KB
55 18 16 4 1 0 0 524288 512KB
56 19 17 1 1 0 0 655360 640KB
57 19 17 2 1 0 0 786432 768KB
58 19 17 3 1 0 0 917504 896KB
59 19 17 4 1 0 0 1048576 1.0MB
60 20 18 1 1 0 0 1310720 1.25MB
61 20 18 2 1 0 0 1572864 1.5MB
62 20 18 3 1 0 0 1835008 1.75MB
63 20 18 4 1 0 0 2097152 2MB
64 21 19 1 1 0 0 2621440 2.5MB
65 21 19 2 1 0 0 3145728 3MB
66 21 19 3 1 0 0 3670016 3.5MB
67 21 19 4 1 0 0 4194304 4MB
68 22 20 1 1 0 0 5242880 5MB
69 22 20 2 1 0 0 6291456 6MB
70 22 20 3 1 0 0 7340032 7MB
71 22 20 4 1 0 0 8388608 8MB
72 23 21 1 1 0 0 10485760 10MB
73 23 21 2 1 0 0 12582912 12MB
74 23 21 3 1 0 0 14680064 14MB
75 23 21 4 1 0 0 16777216 16MB

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

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

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

SizeClasses 体系结构

我们可以把 SizeClasses 看成是一个数组结构,最重要是存储数组索引 index 和 size 的映射关系。当然,还维护了其他数组以避免多次计算。我们先对 SizeClasses 的结构有一个大致的了解,后面再了解重要的 API。
SizeClasses.png
PoolArena 这个大管家通过继承 SizeClasses 拥有内部的数据结构,可以直接调用相关 API。接口 SizeClassMetric 定义了与 SizeClasses 相关的核心的 API。

SizeClassesMetric

SizeClassesMetric_API.png

  1. // io.netty.buffer.SizeClassesMetric
  2. public interface SizeClassesMetric {
  3. /**
  4. * 根据数组索引值获取「size」大小(根据索引直接从「sizeIdx2sizeTab[]」数组读取即可)
  5. *
  6. * @return size
  7. */
  8. int sizeIdx2size(int sizeIdx);
  9. /**
  10. * 根据数组索引值计算「size」大小
  11. *
  12. * @return size
  13. */
  14. int sizeIdx2sizeCompute(int sizeIdx);
  15. /**
  16. * 根据页索引值获取对应的size
  17. *
  18. * @return size which is multiples of pageSize.
  19. */
  20. long pageIdx2size(int pageIdx);
  21. /**
  22. * 根据页索引值计算对应的size
  23. *
  24. * @return size which is multiples of pageSize
  25. */
  26. long pageIdx2sizeCompute(int pageIdx);
  27. /**
  28. * 将请求的大小规格化为最接近的值。
  29. * 比如申请大小为14,则返回数组索引值0,后续通过该数组索引就能得到对应的size值。
  30. *
  31. * @param size request size
  32. *
  33. * @return sizeIdx of the size class
  34. */
  35. int size2SizeIdx(int size);
  36. /**
  37. * 对页的数量进行规格化计算,获取最接近的pages容量大小的数组索引。
  38. * 比如参数pages=5,表示需要分配5*pages大小的内存块。因此会从SizeClass数组中查找对应的索引值。
  39. * @param pages multiples of pageSizes
  40. *
  41. * @return pageIdx of the pageSize class
  42. */
  43. int pages2pageIdx(int pages);
  44. /**
  45. * Normalizes request size down to the nearest pageSize class.
  46. * 对页的数量进行规格化计算,向下取整获取最接近的pages容量大小的数组索引。
  47. *
  48. * @param pages multiples of pageSizes
  49. *
  50. * @return pageIdx of the pageSize class
  51. */
  52. int pages2pageIdxFloor(int pages);
  53. /**
  54. * 对size规格化,内存对齐时使用
  55. * @param size request size
  56. *
  57. * @return normalized size
  58. */
  59. int normalizeSize(int size);
  60. }

上面简单对 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 的索引值。
index size num of page index of pageIdx2sizeTab
31 8192 1 0
35 16384 2 1
37 24576 3 2
39 32KB 4 3
40 40KB 5 4
41 48KB 6 5
42 56KB 7 6
43 64KB 8 7
44 80KB 10 8
45 96KB 12 9
46 112KB 14 10
47 128KB 16 11
48 160KB 20 12
49 192KB 24 13
50 224KB 28 14
51 256KB 32 15
52 320KB 40 16
53 384KB 48 17
54 448KB 56 18
55 512KB 64 19
56 640KB 80 20
57 768KB 96 21
58 896KB 112 22
59 1.0MB 128 23
60 1.25MB 160 24
61 1.5MB 192 25
62 1.75MB 224 26
63 2MB 256 27
64 2.5MB 320 28
65 3MB 384 29
66 3.5MB 448 30
67 4MB 512 31
68 5MB 640 32
69 6MB 768 33
70 7MB 896 34
71 8MB 1024 35
72 10MB 1280 36
73 12MB 1536 37
74 14MB 1792 38
75 16MB 2048 39

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 了。

我的公众号

搜索小道一下