在内存管理(jemalloc3) 这篇文章中,我们介绍了在Netty 4.1.45
版本之前使用的内存分配 jemalloc3
算法。
我们也说了
jemalloc3
算法的一个重大缺陷,就是对于Small
和Normal
规格类型,每个规格内存块相差都是一倍,这就可以导致50%
的内存浪费;例如我们申请513B
大小,那么只能分配1KB
规格的内存块。
一. 划分内存规格
1.1 与 jemalloc3
对比
对比上面两图, jemalloc4
内存分配算法和 jemalloc3
内存分配算法的确有很大不同啊。
jemalloc4
算法将内存分为三种类型:
内存规格 | 描述 |
---|---|
Small |
小规格内存块,容量从16B 到28KB 一共39 个内存规格 |
Normal |
正常规格内存块,容量从32KB 到16MB 一共37 个内存规格 |
Huge |
巨大内存块,不会放在内存管理中,直接内存中申请 |
你会发现没有了
Tiny
类型,而且每个规格内存相隔多少,好像从图中也看不到,只知道一共分为76
个内存规格。
1.2 每个规格的内存大小
index | log2Group | log2Delta | nDelta | isMultiPageSize | isSubPage | log2DeltaLookup | size | log2Size | pageIdxAndPages | size2idxTab |
---|---|---|---|---|---|---|---|---|---|---|
0 | 4 | 4 | 0 | 0 | 1 | 4 | 16 | 4(4) | 0 | |
1 | 4 | 4 | 1 | 0 | 1 | 4 | 32 | 5(5) | 1 | |
2 | 4 | 4 | 2 | 0 | 1 | 4 | 48 | 5(6) | 2 | |
3 | 4 | 4 | 3 | 0 | 1 | 4 | 64 | 6(6) | 3 | |
4 | 6 | 4 | 1 | 0 | 1 | 4 | 80 | 6(7) | 4 | |
5 | 6 | 4 | 2 | 0 | 1 | 4 | 96 | 6(7) | 5 | |
6 | 6 | 4 | 3 | 0 | 1 | 4 | 112 | 6(7) | 6 | |
7 | 6 | 4 | 4 | 0 | 1 | 4 | 128 | 7(7) | 7 | |
8 | 7 | 5 | 1 | 0 | 1 | 5 | 160 | 7(8) | 8->10 | |
9 | 7 | 5 | 2 | 0 | 1 | 5 | 192 | 7(8) | 10->12 | |
10 | 7 | 5 | 3 | 0 | 1 | 5 | 224 | 7(8) | 12->14 | |
11 | 7 | 5 | 4 | 0 | 1 | 5 | 256 | 8(8) | 14->16 | |
12 | 8 | 6 | 1 | 0 | 1 | 6 | 320 | 8(9) | 16->20 | |
13 | 8 | 6 | 2 | 0 | 1 | 6 | 384 | 8(9) | 20->24 | |
14 | 8 | 6 | 3 | 0 | 1 | 6 | 448 | 8(9) | 24->28 | |
15 | 8 | 6 | 4 | 0 | 1 | 6 | 512 | 9(9) | 28->32 | |
16 | 9 | 7 | 1 | 0 | 1 | 7 | 640 | 9(10) | 32->40 | |
17 | 9 | 7 | 2 | 0 | 1 | 7 | 768 | 9(10) | 40->48 | |
18 | 9 | 7 | 3 | 0 | 1 | 7 | 896 | 9(10) | 48->56 | |
19 | 9 | 7 | 4 | 0 | 1 | 7 | 1024 | 10(10) | 56->64 | |
20 | 10 | 8 | 1 | 0 | 1 | 8 | 1280 | 10(11) | 64->80 | |
21 | 10 | 8 | 2 | 0 | 1 | 8 | 1536 | 10(11) | 80->96 | |
22 | 10 | 8 | 3 | 0 | 1 | 8 | 1792 | 10(11) | 96->112 | |
23 | 10 | 8 | 4 | 0 | 1 | 8 | 2048 | 11(11) | 112->128 | |
24 | 11 | 9 | 1 | 0 | 1 | 9 | 2560 | 11(12) | 128->160 | |
25 | 11 | 9 | 2 | 0 | 1 | 9 | 3072 | 11(12) | 160->192 | |
26 | 11 | 9 | 3 | 0 | 1 | 9 | 3584 | 11(12) | 192->224 | |
27 | 11 | 9 | 4 | 0 | 1 | 9 | 4096 | 12(12) | 224->256 | |
28 | 12 | 10 | 1 | 0 | 1 | 0 | 5120 | 12(13) | 无 | |
29 | 12 | 10 | 2 | 0 | 1 | 0 | 6144 | 12(13) | 无 | |
30 | 12 | 10 | 3 | 0 | 1 | 0 | 7168 | 12(13) | 无 | |
31 | 12 | 10 | 4 | 1 | 1 | 0 | 8192 | 13(13) | 0(1) | 无 |
32 | 13 | 11 | 1 | 0 | 1 | 0 | 10240 | 13(14) | 无 | |
33 | 13 | 11 | 2 | 0 | 1 | 0 | 12288 | 13(14) | 无 | |
34 | 13 | 11 | 3 | 0 | 1 | 0 | 14336 | 13(14) | 无 | |
35 | 13 | 11 | 4 | 1 | 1 | 0 | 16384 | 14(14) | 1(2) | 无 |
36 | 14 | 12 | 1 | 0 | 1 | 0 | 20480 | 14(15) | 无 | |
37 | 14 | 12 | 2 | 1 | 1 | 0 | 24576 | 14(15) | 2(3) | 无 |
38 | 14 | 12 | 3 | 0 | 1 | 0 | 28672 | 14(15) | 无 | |
39 | 14 | 12 | 4 | 1 | 0 | 0 | 32768 | 15(15) | 3(4) | 无 |
40 | 15 | 13 | 1 | 1 | 0 | 0 | 40960 | 15(16) | 4(5) | 无 |
41 | 15 | 13 | 2 | 1 | 0 | 0 | 49152 | 15(16) | 5(6) | 无 |
42 | 15 | 13 | 3 | 1 | 0 | 0 | 57344 | 15(16) | 6(7) | 无 |
43 | 15 | 13 | 4 | 1 | 0 | 0 | 65536 | 16(16) | 7(8->9) | 无 |
44 | 16 | 14 | 1 | 1 | 0 | 0 | 81920 | 16(17) | 8(10->11) | 无 |
45 | 16 | 14 | 2 | 1 | 0 | 0 | 98304 | 16(17) | 9(12->13) | 无 |
46 | 16 | 14 | 3 | 1 | 0 | 0 | 114688 | 16(17) | 10(14->15) | 无 |
47 | 16 | 14 | 4 | 1 | 0 | 0 | 131072 | 17(17) | 11(16->19) | 无 |
48 | 17 | 15 | 1 | 1 | 0 | 0 | 163840 | 17(18) | 12(20->23) | 无 |
49 | 17 | 15 | 2 | 1 | 0 | 0 | 196608 | 17(18) | 13(24->27) | 无 |
50 | 17 | 15 | 3 | 1 | 0 | 0 | 229376 | 17(18) | 14(28->31) | 无 |
51 | 17 | 15 | 4 | 1 | 0 | 0 | 262144 | 18(18) | 15(32->39) | 无 |
52 | 18 | 16 | 1 | 1 | 0 | 0 | 327680 | 18(19) | 16(40->47) | 无 |
53 | 18 | 16 | 2 | 1 | 0 | 0 | 393216 | 18(19) | 17(48->55) | 无 |
54 | 18 | 16 | 3 | 1 | 0 | 0 | 458752 | 18(19) | 18(56->63) | 无 |
55 | 18 | 16 | 4 | 1 | 0 | 0 | 524288 | 19(19) | 19(64->79) | 无 |
56 | 19 | 17 | 1 | 1 | 0 | 0 | 655360 | 19(20) | 20(80->95) | 无 |
57 | 19 | 17 | 2 | 1 | 0 | 0 | 786432 | 19(20) | 21(96->111) | 无 |
58 | 19 | 17 | 3 | 1 | 0 | 0 | 917504 | 19(20) | 22(112->127) | 无 |
59 | 19 | 17 | 4 | 1 | 0 | 0 | 1048576 | 20(20) | 23(128->159) | 无 |
60 | 20 | 18 | 1 | 1 | 0 | 0 | 1310720 | 20(21) | 24(160->191) | 无 |
61 | 20 | 18 | 2 | 1 | 0 | 0 | 1572864 | 20(21) | 25(192->223) | 无 |
62 | 20 | 18 | 3 | 1 | 0 | 0 | 1835008 | 20(21) | 26(224->255) | 无 |
63 | 20 | 18 | 4 | 1 | 0 | 0 | 2097152 | 21(21) | 27(256->319) | 无 |
64 | 21 | 19 | 1 | 1 | 0 | 0 | 2621440 | 21(22) | 28(320->383) | 无 |
65 | 21 | 19 | 2 | 1 | 0 | 0 | 3145728 | 21(22) | 29(384->447) | 无 |
66 | 21 | 19 | 3 | 1 | 0 | 0 | 3670016 | 21(22) | 30(448->511) | 无 |
67 | 21 | 19 | 4 | 1 | 0 | 0 | 4194304 | 22(22) | 31(512->639) | 无 |
68 | 22 | 20 | 1 | 1 | 0 | 0 | 5242880 | 22(23) | 32(640->767) | 无 |
69 | 22 | 20 | 2 | 1 | 0 | 0 | 6291456 | 22(23) | 33(768->895) | 无 |
70 | 22 | 20 | 3 | 1 | 0 | 0 | 7340032 | 22(23) | 34(896->1023) | 无 |
71 | 22 | 20 | 4 | 1 | 0 | 0 | 8388608 | 23(23) | 35(1024->1279) | 无 |
72 | 23 | 21 | 1 | 1 | 0 | 0 | 10485760 | 23(24) | 36(1280->1535) | 无 |
73 | 23 | 21 | 2 | 1 | 0 | 0 | 12582912 | 23(24) | 37(1536->1791) | 无 |
74 | 23 | 21 | 3 | 1 | 0 | 0 | 14680064 | 23(24) | 38(1792->2047) | 无 |
75 | 23 | 21 | 4 | 1 | 0 | 0 | 16777216 | 24(24) | 39(2048) | 无 |
先解释每个表头的含义:
-
index
: 内存规格的索引,一共有76
个内存规格,因此索引就是0 -> 75
。 -
log2Group
: 每一个内存规格组的基础大小的log2
对数。 -
log2Delta
: 每一个内存规格组组内内存增量的log2
对数。 -
nDelta
: 每一个内存规格组增量的个数。 -
isMultiPageSize
: 表示是不是pageSize
的倍数。 -
isSubPage
: 表示是不是isSubPage
类型,就是Small
类型。 -
log2DeltaLookup
: 对于方便较小size
快速查询对应规格大小的辅助值。 -
size
: 表示这个内存规格对应的内存大小。 -
log2Size
: 表示这个内存规格对应内存大小的log2
对数,括号里的数是能容纳这个内存大小最近的log2
对数。例如
48
, 它的log2
对数是5
,但是能容纳48
最近的log2
对数却是6
。 -
pageIdxAndPages
: 表示pages
对应的下标,括号里面是它的范围。因为内存规格增量会越来越大,后面会出现两个内存规格差值是
pageSize
的几倍,所以里面就表示它的范围。 -
size2idxTab
: 是用来方便查找较小size
快速查询对应规格大小。
仔细观察表中数据,我们得出如下特点:
- 每一组有
4
种内存规格,组内每一种内存规格的差值是一样的,大小都是(1 << log2Delta
)。 - 除了第一组外,每一组的
log2Group
和log2Delta
都比上一组增加了1
。 - 除了第一组外,其他组的
log2Group == log2Delta + 2
,第一组log2Group == log2Delta
。 - 除了第一组外,其他组的
nDelta
都是从1
开始的,第一组是从0
开始的。 - 除了第一组外,其他组第一个内存规格正好是上一个组第一个内存规格大小的两倍。
我们知道内存规格内存大小的计算公式
size = 1 << log2Group + nDelta * (1 << log2Delta)
除第一组外,每一组的 nDelta
从1
开始,log2Group == log2Delta + 2
,并且每一组 log2Group
和 log2Delta
都比上一组增加了 1
;因此每一组第一个内存规格正好是上一个组第一个内存规格大小的两倍。
二. SizeClasses
类
这个类的作用就是处理 jemalloc4
算法的内存大小分配的。
2.1 重要属性
-
short[][] sizeClasses
:是一个二维数组,一共是76
个 长度为7
的short[]
一维数组组成。short[7]
就存储上面表格前7
标头数据。 -
int[] sizeIdx2sizeTab
:用来记录index
对应的内存规格大小。数组长度是
76
,存储了上面表格size
的数据,数组下标就是index
。 -
int[] pageIdx2sizeTab
: 用来记录pageIdxAndPages
对应的内存规格大小。数组长度是
40
,存储了上面表格size
的数据,数组下标就是pageIdxAndPages
。 -
int[] size2idxTab
: 用来较小size
对应sizeIdx
的值,这样我们再查找较小值对应的内存规格时,可以直接通过这个数组得到。数组长度是
256
,就是上面表格size2idxTab
相关值。
例如,我们求66
对应的内存规格,(66 - 1)/16 = 3
, 通过size2idxTab
得到sizeIdx
也是3
;如果是257
,(257 - 1)/16 = 16
,通过size2idxTab
得到sizeIdx
也是11
。
2.2 重要属方法
public interface SizeClassesMetric {
/**
* 根据给定的数组索引 [sizeIdx] 从 [sizeIdx2sizeTab] 数组中获取对应的内存大小
*
* @return size
*/
int sizeIdx2size(int sizeIdx);
/**
* 根据给定的数组索引 [sizeIdx],计算出对应的内存大小
*
* @return size
*/
int sizeIdx2sizeCompute(int sizeIdx);
/**
* 根据给定的数组索引 [pageIdx] 从 [pageIdx2sizeTab] 数组中获取对应的内存大小
*
* @return size which is multiples of pageSize.
*/
long pageIdx2size(int pageIdx);
/**
* 根据给定的数组索引 [pageIdx],计算出对应的内存大小
*
* @return size which is multiples of pageSize
*/
long pageIdx2sizeCompute(int pageIdx);
/**
*
* 根据请求大小 [size] 得到规格化内存的索引值[sizeIdx]
*
*/
int size2SizeIdx(int size);
/**
*
* 根据请求大小 [pages] 得到规格化内存的索引值[pageIdx]
*
* @return pageIdx of the pageSize class
*/
int pages2pageIdx(int pages);
/**
* 根据请求大小 [pages] 得到规格化内存的索引值[pageIdx],
* 与 pages2pageIdx(int pages) 方法不同,它会向下求得最近的[pageIdx]。
*
*/
int pages2pageIdxFloor(int pages);
/**
* 以指定的大小和对齐方式分配对象,使可用大小标准化。
*/
int normalizeSize(int size);
}
2.2.1 sizeIdx2size
方法
@Override
public int sizeIdx2size(int sizeIdx) {
return sizeIdx2sizeTab[sizeIdx];
}
2.2.2 sizeIdx2sizeCompute
方法
@Override
public int sizeIdx2sizeCompute(int sizeIdx) {
/**
* LOG2_SIZE_CLASS_GROUP = 2: 说明每一组group的数量就是4个;
* LOG2_QUANTUM = 4: 最小容量从16,即2的4次方开始。
* 计算 size 的值,必须要区分第一组和后面的组,这个计算逻辑不同。
*/
// 因为每一组数量就是4, >> LOG2_SIZE_CLASS_GROUP 就是得到组group;
// 0 表示第一组,1就是第二组
int group = sizeIdx >> LOG2_SIZE_CLASS_GROUP;
// (1 << LOG2_SIZE_CLASS_GROUP) - 1 = 3;
// 和 sizeIdx 进行位与(&) 运算就是得到整除4的余数
int mod = sizeIdx & (1 << LOG2_SIZE_CLASS_GROUP) - 1;
/**
* 如果是第一组(group == 0),那么 groupSize就是0,最后size只通过 modSize 得到;
* 其他组,那么组的基础size: 1 << ( 5 + group), LOG2_QUANTUM + LOG2_SIZE_CLASS_GROUP - 1 的值就是 5,
* 这个计算公式就相当于 1 << log2Group; log2Group = 5 + group
*/
int groupSize = group == 0? 0 :
1 << LOG2_QUANTUM + LOG2_SIZE_CLASS_GROUP - 1 << group;
// 第一组 shift == 1
// 其他组 shift = group,其实第二组也是 shift == group == 1
int shift = group == 0? 1 : group;
// lgDelta 就是 log2Delta 的值,第一组和第二组都是4,第三组是5
int lgDelta = shift + LOG2_QUANTUM - 1;
/**
* 除第一组外,其他组的 nDelta 都是从1开始的,所以 nDelta = mod + 1;
* 第一组的 nDelta 从0开始的,但是这里也使用了 mod + 1,那是因为我们直接将 groupSize = 0
*/
int modSize = mod + 1 << lgDelta;
return groupSize + modSize;
}
2.2.3 pageIdx2size
方法
@Override
public long pageIdx2size(int pageIdx) {
return pageIdx2sizeTab[pageIdx];
}
2.2.4 pageIdx2sizeCompute
方法
@Override
public long pageIdx2sizeCompute(int pageIdx) {
/**
* 这个的逻辑和 sizeIdx2sizeCompute() 几乎一模一样,
* 唯一不同的是 sizeIdx2sizeCompute() 是从 LOG2_QUANTUM (4) 开始
* 而这个方法是从 pageShifts (pageSize 13) 开始
*/
int group = pageIdx >> LOG2_SIZE_CLASS_GROUP;
int mod = pageIdx & (1 << LOG2_SIZE_CLASS_GROUP) - 1;
long groupSize = group == 0? 0 :
1L << pageShifts + LOG2_SIZE_CLASS_GROUP - 1 << group;
int shift = group == 0? 1 : group;
int log2Delta = shift + pageShifts - 1;
int modSize = mod + 1 << log2Delta;
return groupSize + modSize;
}
2.2.5 size2SizeIdx
方法
/**
* 根据请求 size 得到对应的 sizeIdx,
* 这个 sizeIdx 就是 sizeIdx2sizeTab 数组的下标。
* 其实就是得到这个 size 对应的规格化内存块大小
*/
@Override
public int size2SizeIdx(int size) {
if (size == 0) {
return 0;
}
// 如果大于 chunkSize,说明它是一个 Huge 类型内存块;
// 那么返回 nSizes,超出 sizeIdx2sizeTab 数组的范围。
if (size > chunkSize) {
return nSizes;
}
// 是否需要进行内存对齐,就是 size 必须是 directMemoryCacheAlignment 的倍数。
// directMemoryCacheAlignment 必须是 2 的幂数,这样才能进行位运算。
if (directMemoryCacheAlignment > 0) {
size = alignSize(size);
}
// 如果 size 小于等于 lookupMaxSize,
// 那么就在 size2idxTab 数组范围内,
// 可以通过 size2idxTab 数组快速得到 sizeIdx 值。
if (size <= lookupMaxSize) {
// >> LOG2_QUANTUM 相当于 (size - 1) / 16,因为肯定都是 16 的倍数
// 获取 sizeIdx 再通过 sizeIdx2sizeTab 数组得到对应大小
return size2idxTab[size - 1 >> LOG2_QUANTUM];
}
/**
* ((size << 1) - 1) 是向上得到 size 对应的 log2 的数;
* 例如 8 得到 3,9 得到 4,15 得到 4, 16 得到 4, 17 得到 5.
*
* LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1 的值就是 7。
* 而第一组规格内存块 size 最大就是 64, 计算后的值就是 6;
* 如果我们需要大小为 65,计算后的值就是 7, 在第二组的第一个规格内存块。
*
* 因此我们可以得出,
* 只要 x 小于 7,这个 size 就是在第一组内;
* x 大于等于 7,这个 size 在除第一组外的其他组内。
*
* 第一组和其他组的算法不一样
*/
int x = log2((size << 1) - 1);
// 判断是第一组还是其他组。
// 第一组 shift 是 0,第二组是 1, 第三组是 2,依次递增1
int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);
// 因为每一组默认是 4 个,就是 shift << LOG2_SIZE_CLASS_GROUP,
// 得到这一组的开始值。
int group = shift << LOG2_SIZE_CLASS_GROUP;
// 判断是第一组还是其他组。
// 第一组就是 4, 第二组也是 4,第三组是5,依次递增1
int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;
int deltaInverseMask = -1 << log2Delta;
int mod = (size - 1 & deltaInverseMask) >> log2Delta &
(1 << LOG2_SIZE_CLASS_GROUP) - 1;
return group + mod;
}
2.2.6 pages2pageIdx
和 pages2pageIdxFloor
方法
@Override
public int pages2pageIdx(int pages) {
return pages2pageIdxCompute(pages, false);
}
@Override
public int pages2pageIdxFloor(int pages) {
return pages2pageIdxCompute(pages, true);
}
private int pages2pageIdxCompute(int pages, boolean floor) {
int pageSize = pages << pageShifts;
if (pageSize > chunkSize) {
return nPSizes;
}
int x = log2((pageSize << 1) - 1);
int shift = x < LOG2_SIZE_CLASS_GROUP + pageShifts
? 0 : x - (LOG2_SIZE_CLASS_GROUP + pageShifts);
int group = shift << LOG2_SIZE_CLASS_GROUP;
int log2Delta = x < LOG2_SIZE_CLASS_GROUP + pageShifts + 1?
pageShifts : x - LOG2_SIZE_CLASS_GROUP - 1;
int deltaInverseMask = -1 << log2Delta;
int mod = (pageSize - 1 & deltaInverseMask) >> log2Delta &
(1 << LOG2_SIZE_CLASS_GROUP) - 1;
int pageIdx = group + mod;
// floor 是true, 当 pageIdx 对应的内存大小比 pages 大时,pageIdx要减一
if (floor && pageIdx2sizeTab[pageIdx] > pages << pageShifts) {
pageIdx--;
}
return pageIdx;
}
三. PoolChunk
方法
先理解重要概念:
-
page
: 它是PoolChunk
能分配内存块的最小单位。 -
run
: 一个run
中包含多个page
;当然也可以只包含一个page
,就是单page
的run
。 -
chunk
: 就是表示这个PoolChunk
,包含多个run
。
PoolChunk
进行内存块的分配,就是分配出不同的 run
,如下图:
* /-----------------\
* | run |
* | |
* | |
* |-----------------|
* | run |
* | |
* |-----------------|
* | unalloctated |
* | (freed) |
* | |
* |-----------------|
* | subpage |
* |-----------------|
* | unallocated |
* | (freed) |
* | ... |
* | ... |
* | ... |
* | |
* | |
* | |
* \-----------------/
3.1 重要属性
3.1.1 handle
那么怎么代表不同的 run
呢,就是用一个 long
类型的 handle
来表示:
oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
handle
是 long
类型,它的不同 bits
的意义如下:
-
o
:前十五位bit
表示这个内存块run
的页偏移量runOffset
。因为
PoolChunk
是按照页来进行分割的,页偏移量runOffset
表示这个内存块run
在这个PoolChunk
第runOffset
页的位置。 -
s
:中间十五位bit
表示这个内存块run
拥有的页数size
。 -
u
:一位bit
表示这个内存块run
是否被使用。 -
e
:一位bit
表示这个内存块run
是否是isSubpage
。 -
b
: 一共32位,表示subpage
的bitmapIdx
。
3.1.2 runsAvail
private final LongPriorityQueue[] runsAvail;
- 它是一个优先级队列
LongPriorityQueue
的数组,数组长度就是pageIdx
数组的大小,默认就是40
。- 它管理者所有可分配的
run
内存块。run
根据拥有的页数不同可以分为40
中,所以runsAvail
中每一个优先级队列管理一种类型run
内存块。LongPriorityQueue
是一个优先级队列,利用堆排序来实现的,优先级就是run
内存块的页偏移量runOffset
。- 初始时,
runsAvail
在下标39
的优先级队列中存放这个整块PoolChunk
的run
内存块,即runOffset
是0
,size
是chunkSize/pageSize
。
3.1.3 runsAvailMap
private final LongLongHashMap runsAvailMap;
- 将它看成一个
Map
,存放着所有可分配的run
内存块第一个和最后一个页偏移量runOffset
,以及run
内存块的handle
。runsAvailMap
中的key
就是页偏移量runOffset
,value
就是内存块的handle
。- 它作用就是当释放内存块
run
的时候,可以将相连可分配合并在一起。
3.1.4 subpages
private final PoolSubpage<T>[] subpages
- 管理这个
PoolChunk
下的所有PoolSubpage
。- 特别注意
PoolSubpage
的大小不固定是pageSize
,有可能是几个pageSize
的大小。
3.2 分配内存块
3.2.1 allocate
方法
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {
final long handle;
if (sizeIdx <= arena.smallMaxSizeIdx) {
// small
handle = allocateSubpage(sizeIdx);
if (handle < 0) {
return false;
}
// 肯定是 isSubpage
assert isSubpage(handle);
} else {
// normal
// runSize must be multiple of pageSize
// 是 Normal 类型,先根据 sizeIdx 获取对应的规格内存块大小,肯定是 pageSize 的倍数
int runSize = arena.sizeIdx2size(sizeIdx);
handle = allocateRun(runSize);
if (handle < 0) {
return false;
}
}
ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;
initBuf(buf, nioBuffer, handle, reqCapacity, cache);
return true;
}
- 如果是
Small
规格类型,那么就调用allocateSubpage(sizeIdx)
方法,分配内存块。注意Small
类型最大是28KB
,超过pageSize
的大小。- 如果是
Normal
规格类型,那么就调用allocateRun(runSize)
方法,分配内存块。
3.2.2 allocateRun
方法
private long allocateRun(int runSize) {
int pages = runSize >> pageShifts;
// 根据需要的 pages 得到对应的 pageIdx,
// 也就是 run 的规格
int pageIdx = arena.pages2pageIdx(pages);
synchronized (runsAvail) {
//find first queue which has at least one big enough run
// 寻找第一个足够大能分配需求 run 规格的内存块
int queueIdx = runFirstBestFit(pageIdx);
if (queueIdx == -1) {
// 表示当前 PoolChunk 已经没有足够的空间分配这个 runSize
return -1;
}
//get run with min offset in this queue
LongPriorityQueue queue = runsAvail[queueIdx];
// 得到这个能分配的内存块,这里其实已经从 queue 中删除了这个内存块了
long handle = queue.poll();
// 这个内存块肯定是有效的
assert handle != LongPriorityQueue.NO_VALUE && !isUsed(handle) : "invalid handle: " + handle;
// 从队列中移除这个可用内存块
removeAvailRun(queue, handle);
if (handle != -1) {
// 将这个可用内存块分割成两部分:
// 一块是 pages 大小,返回给调用者使用。
// 一块是剩余大小,表示还能分配内存块,还需要存入 runsAvail 中
handle = splitLargeRun(handle, pages);
}
// runSize(pageShifts, handle) 表示返回内存块的大小
// 当前 PoolChunk 的可用字节数要减少
freeBytes -= runSize(pageShifts, handle);
return handle;
}
}
- 从
runsAvail
中寻找足够大能分配需求可分配run
内存块。- 调用
splitLargeRun(handle, pages)
将可用内存块分割成两部分,一块是pages
大小,返回给调用者使用;一块是剩余大小,表示还能分配内存块,还需要存入runsAvail
中。
3.2.3 allocateSubpage
方法
private long allocateSubpage(int sizeIdx) {
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
synchronized (head) {
//allocate a new run
// 计算得到的值 runSize 必须是 pageSize 的倍数,
int runSize = calculateRunSize(sizeIdx);
//runSize must be multiples of pageSize
// runSize 必须是 pageSize 的倍数,
// 因为分配可用内存块 run 就是按照几个 pageSize 分配的
long runHandle = allocateRun(runSize);
if (runHandle < 0) {
return -1;
}
int runOffset = runOffset(runHandle);
assert subpages[runOffset] == null;
int elemSize = arena.sizeIdx2size(sizeIdx);
PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
runSize(pageShifts, runHandle), elemSize);
subpages[runOffset] = subpage;
// 返回 handle 是isSubpage, 且包含 bitmapIdx
return subpage.allocate();
}
}
- 调用
calculateRunSize(sizeIdx)
方法,计算出即能整除需要的Small
类型内存大小,又能整除pageSize
的最小值。因为我们分配run
内存块大小只能是pageSize
的倍数。- 调用
allocateRun(runSize)
方法分配run
内存块。- 创建
PoolSubpage
对象,分配需求的Small
类型内存块。
3.2.3 initBuf
方法
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,
PoolThreadCache threadCache) {
if (isRun(handle)) {
// 如果是 Normal 规格类型
// 真实偏移量就是 handle 的 (runOffset * pageSize)
// 大小就是 handle 的 (runSize * pageSize)
buf.init(this, nioBuffer, handle, runOffset(handle) << pageShifts,
reqCapacity, runSize(pageShifts, handle), arena.parent.threadCache());
} else {
initBufWithSubpage(buf, nioBuffer, handle, reqCapacity, threadCache);
}
}
void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,
PoolThreadCache threadCache) {
int runOffset = runOffset(handle);
int bitmapIdx = bitmapIdx(handle);
PoolSubpage<T> s = subpages[runOffset];
assert s.doNotDestroy;
assert reqCapacity <= s.elemSize;
// 真实偏移量就是handle 的 ((runOffset * pageSize) + (bitmapIdx * s.elemSize))
// 大小就是 elemSize
int offset = (runOffset << pageShifts) + bitmapIdx * s.elemSize;
buf.init(this, nioBuffer, handle, offset, reqCapacity, s.elemSize, threadCache);
}
初始化池缓冲区
PooledByteBuf
, 只需要计算出偏移量和大小就可以了。
3.2.4 runFirstBestFit
/**
* 根据 pageIdx 寻找第一个足够大规格的 run
*/
private int runFirstBestFit(int pageIdx) {
if (freeBytes == chunkSize) {
// 如果这个 PoolChunk 还没有进行分配,直接
return arena.nPSizes - 1;
}
// 从刚满足run规格的 pageIdx 开始,一直遍历到最大run 规格(arena.nPSizes -1)
// 从 PoolChunk 中寻找能分配的 run
for (int i = pageIdx; i < arena.nPSizes; i++) {
LongPriorityQueue queue = runsAvail[i];
if (queue != null && !queue.isEmpty()) {
return i;
}
}
return -1;
}
3.2.5 splitLargeRun
private long splitLargeRun(long handle, int needPages) {
assert needPages > 0;
// 获取这个可用内存块 run 一共有多少个 pages
int totalPages = runPages(handle);
assert needPages <= totalPages;
// 还剩余的内存块大小
int remPages = totalPages - needPages;
if (remPages > 0) {
// 得到内存块 run 的偏移量
int runOffset = runOffset(handle);
// keep track of trailing unused pages for later use
// runOffset + needPages 表示剩余可用内存块的偏移量
int availOffset = runOffset + needPages;
// 得到新的剩余可用的内存块 run 的handle
long availRun = toRunHandle(availOffset, remPages, 0);
// 将剩余可用的内存块 run 存到 runsAvail 中
insertAvailRun(availOffset, remPages, availRun);
// 返回给调用者的内存块 run 的handle
return toRunHandle(runOffset, needPages, 1);
}
//mark it as used
// 将这个 handle 变成已使用
handle |= 1L << IS_USED_SHIFT;
return handle;
}
3.2.6 calculateRunSize
/**
* 计算得到 runSize 必须是 pageSize 的倍数
*/
private int calculateRunSize(int sizeIdx) {
int maxElements = 1 << pageShifts - SizeClasses.LOG2_QUANTUM; // 512
int runSize = 0;
int nElements;
// 得到 sizeIdx 对应规格化内存块的大小
final int elemSize = arena.sizeIdx2size(sizeIdx);
//find lowest common multiple of pageSize and elemSize
// 就是找到最小 pageSize 的倍数 runSize,能够整除 elemSize
// runSize != (runSize / elemSize) * elemSize 就表示 runSize 能够整除 elemSize
do {
runSize += pageSize;
nElements = runSize / elemSize;
} while (nElements < maxElements && runSize != nElements * elemSize);
while (nElements > maxElements) {
runSize -= pageSize;
nElements = runSize / elemSize;
}
assert nElements > 0;
assert runSize <= chunkSize;
assert runSize >= elemSize;
return runSize;
}
3.3 释放内存块
3.3.1 free
void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
if (isSubpage(handle)) {
// 如果是 small 类型
int sizeIdx = arena.size2SizeIdx(normCapacity);
// 找到 PoolArena 中这个small 规格类型对应 PoolSubpage 链表
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
// 从 handle 中得到这个内存块 run 的偏移量 runOffset
int sIdx = runOffset(handle);
// 通过这个偏移量 runOffset 得到对应的 PoolSubpage
PoolSubpage<T> subpage = subpages[sIdx];
assert subpage != null && subpage.doNotDestroy;
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
synchronized (head) {
if (subpage.free(head, bitmapIdx(handle))) {
//the subpage is still used, do not free it
// 这个 PoolSubpage 还在使用,不能释放这块内存块 run
return;
}
assert !subpage.doNotDestroy;
// Null out slot in the array as it was freed and we should not use it anymore.
// 这里和之前不一样,直接设置为null,的确不需要复用,没有多大意义
subpages[sIdx] = null;
}
}
// 程序运行到这里肯定需要是否这个 handle 对应的内存块 run
int pages = runPages(handle);
synchronized (runsAvail) {
// collapse continuous runs, successfully collapsed runs
// will be removed from runsAvail and runsAvailMap
// 在这个要释放的内存块周围,不断尝试向前或者向后合并可用内存块,
// 组成一个大的内存块
long finalRun = collapseRuns(handle);
//set run as not used
// 设置成未使用
finalRun &= ~(1L << IS_USED_SHIFT);
//if it is a subpage, set it to run
// 设置成非 subpage
finalRun &= ~(1L << IS_SUBPAGE_SHIFT);
// 将这个可用内存块run 插入到 runsAvail 中
insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);
// 增加当前 PoolChunk 的可用字节数
// 注意,这里一定不能用 runPages(finalRun),
// 因为虽然我们可能合并了一个大的可用内存块,但是被合并的内存块原来就是可用的。
freeBytes += pages << pageShifts;
}
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}
- 如果是
small
类型,那么调用subpage.free()
方法进行释放。- 释放
normal
类型或者small
类型已经完成释放了,那么就要释放这个run
内存块。- 调用
collapseRuns(handle)
方法来合并可分配内存块。- 调用
insertAvailRun(...)
方法将内存块插入到runsAvail
中。
3.3.2 collapseRuns
/**
* 不断向前和向后合并可用内存块
*/
private long collapseRuns(long handle) {
return collapseNext(collapsePast(handle));
}
/**
* 不断向前尝试合并可用内存块
*/
private long collapsePast(long handle) {
// 通过循环,不断向前尝试合并
for (;;) {
// 得到当前 handle 对应内存块的偏移量 runOffset
int runOffset = runOffset(handle);
// 得到当前 handle 对应内存块的大小 runPages
int runPages = runPages(handle);
/**
* 判断释放内存块 run 的前面有没有可用内存块
*/
long pastRun = getAvailRunByOffset(runOffset - 1);
if (pastRun == -1) {
// 前面没有可用内存块,就直接返回
return handle;
}
// 前面可用内存块 run 的偏移量pastOffset
int pastOffset = runOffset(pastRun);
// 前面可用内存块 run 的大小 pastPages
int pastPages = runPages(pastRun);
//is continuous
if (pastRun != handle && pastOffset + pastPages == runOffset) {
// 如果前一个可用内存块 run (pastOffset + pastPages == runOffset),
// 说明前一个可用内存块和当前释放的内存块是连续的,就需要合并。
// 删除前一个可用内存块,因为它要被合并
removeAvailRun(pastRun);
// 创建新的包含前一个可用内存块的新合并内存块。
handle = toRunHandle(pastOffset, pastPages + runPages, 0);
} else {
return handle;
}
}
}
/**
* 不断向后尝试合并可用内存块
*/
private long collapseNext(long handle) {
// 通过循环,不断向后尝试合并
for (;;) {
int runOffset = runOffset(handle);
int runPages = runPages(handle);
/**
* 判断释放内存块 run 的后面有没有可用内存块
*/
long nextRun = getAvailRunByOffset(runOffset + runPages);
if (nextRun == -1) {
return handle;
}
int nextOffset = runOffset(nextRun);
int nextPages = runPages(nextRun);
//is continuous
if (nextRun != handle && runOffset + runPages == nextOffset) {
// 如果后一个可用内存块 run (runOffset + runPages == nextOffset),
// 说明后一个可用内存块和当前释放的内存块是连续的,就需要合并。
// 删除后一个可用内存块,因为它要被合并
removeAvailRun(nextRun);
handle = toRunHandle(runOffset, runPages + nextPages, 0);
} else {
return handle;
}
}
}
就是查看要释放的内存块,前后有没有连续的可分配内存块,有的话,就合并成一个大的内存块。