概述
前面已对 Netty 使用 jemalloc3(jemalloc3、jemalloc4 指代 Netty 实现的 Java 版本,而非 C) 实现的内存分配的思路以及源码进行详解,接下来的这两篇是详解 Netty 基于 jemalloc4 重构内存分配的思想以及源码。jemalloc4 相较于 jemalloc3 最大的提升是进一步优化内存碎片问题,因为在 jemalloc3 中最多可能会导致 50% 内存碎片,但 jemalloc4 通过划分更细粒度的内存规格在一定程度上改善了这一问题,这也是 SizeClasses 的由来。相关问题可见 Netty issue 10267 和 jemalloc changeLog。
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
这是一个极其重要类,它在内部维护一个二维数组,这个数组存储与内存规格有关的详细信息。我们先看看这张表长什么样子的:
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 体系结构
我们可以把 SizeClasses 看成是一个数组结构,最重要是存储数组索引 index 和 size 的映射关系。当然,还维护了其他数组以避免多次计算。我们先对 SizeClasses 的结构有一个大致的了解,后面再了解重要的 API。
PoolArena 这个大管家通过继承 SizeClasses 拥有内部的数据结构,可以直接调用相关 API。接口 SizeClassMetric 定义了与 SizeClasses 相关的核心的 API。
SizeClassesMetric
// io.netty.buffer.SizeClassesMetric
public interface SizeClassesMetric {
/**
* 根据数组索引值获取「size」大小(根据索引直接从「sizeIdx2sizeTab[]」数组读取即可)
*
* @return size
*/
int sizeIdx2size(int sizeIdx);
/**
* 根据数组索引值计算「size」大小
*
* @return size
*/
int sizeIdx2sizeCompute(int sizeIdx);
/**
* 根据页索引值获取对应的size
*
* @return size which is multiples of pageSize.
*/
long pageIdx2size(int pageIdx);
/**
* 根据页索引值计算对应的size
*
* @return size which is multiples of pageSize
*/
long pageIdx2sizeCompute(int pageIdx);
/**
* 将请求的大小规格化为最接近的值。
* 比如申请大小为14,则返回数组索引值0,后续通过该数组索引就能得到对应的size值。
*
* @param size request size
*
* @return sizeIdx of the size class
*/
int size2SizeIdx(int size);
/**
* 对页的数量进行规格化计算,获取最接近的pages容量大小的数组索引。
* 比如参数pages=5,表示需要分配5*pages大小的内存块。因此会从SizeClass数组中查找对应的索引值。
* @param pages multiples of pageSizes
*
* @return pageIdx of the pageSize class
*/
int pages2pageIdx(int pages);
/**
* Normalizes request size down to the nearest pageSize class.
* 对页的数量进行规格化计算,向下取整获取最接近的pages容量大小的数组索引。
*
* @param pages multiples of pageSizes
*
* @return pageIdx of the pageSize class
*/
int pages2pageIdxFloor(int pages);
/**
* 对size规格化,内存对齐时使用
* @param size request size
*
* @return normalized size
*/
int normalizeSize(int size);
}
上面简单对 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 了。
我的公众号
搜索小道一下。