Netty-内存池源码三 (SizeClasses终结)
内存池核心类如下:
- PooledByteBufAllocator
- PooledUnsafeDirectByteBuf
- PooledUnsafeDirectByteBuf
- PoolThreadCache
- MemoryRegionCache
- PoolArena
- SizeClasses 本期介绍
- PoolChunk
- LongPriorityQueue
- LongLongHashMap
- PoolSubpage
上期 大致了解了 【SizeClasses
】 内主要的工作就是 生成 4 张表,供后面的内存分配时使用。 可以更加详细的划分内存规格,减少内存碎片。
本期就是对 SizeClasses 类的源码进行一个透彻的分析, 主要要搞清楚 jemalloc4 这样修改有什么好处。
成员变量
首先来看下关键的成员变量
// 因为sizeClass规格中最小的就是 16B, 因此这里就是定义最小的规格 log2(16) = 4
// 最小规格的移位
static final int LOG2_QUANTUM = 4;
// 因为sizeClasses总表中 是按照每4行为1组,所以 log2(4) = 2
// 定义sizeClasses中 4行 为一组,移位为2
private static final int LOG2_SIZE_CLASS_GROUP = 2;
// 因为size2idxTab 详细划分查找表中 最大限制size大小为4096,所以这里 log2(4096) = 12
// 定义最大详细划分查找表的规格限制为4096,移位为12
private static final int LOG2_MAX_LOOKUP_SIZE = 12;
// 以下7个常量,对应着 sizeClasses总表的 7个列字段
// index
private static final int INDEX_IDX = 0;
// log2Group
private static final int LOG2GROUP_IDX = 1;
// log2Delta
private static final int LOG2DELTA_IDX = 2;
// nDelta
private static final int NDELTA_IDX = 3;
// pageSize => isMultiPageSize
private static final int PAGESIZE_IDX = 4;
// subpPage
private static final int SUBPAGE_IDX = 5;
// log2DeltaLookup
private static final int LOG2_DELTA_LOOKUP_IDX = 6;
// 定义常量 yes no
private static final byte no = 0, yes = 1;
// 一页的大小 默认8KB
protected final int pageSize;
// 一页的移位 默认13
protected final int pageShifts;
// 一chunk的大小 默认16mb
protected final int chunkSize;
// 对齐填充 0
protected final int directMemoryCacheAlignment;
// 总规格数量 默认 76
final int nSizes;
// subPage的数量 默认39
int nSubpages;
// 页倍数的规格数量 默认40
int nPSizes;
// 记录最大Subpage的 index 默认是38
int smallMaxSizeIdx;
// 4K(4096)
private int lookupMaxSize;
// sizeClasses 总表
private final short[][] sizeClasses;
// 规格大小为页的倍数 的表 记录 pageIndex->size
private final int[] pageIdx2sizeTab;
// 查询表 记录所有规格的 index=>size 的表
private final int[] sizeIdx2sizeTab;
// lookup table used for size <= lookupMaxclass
// spacing is 1 << LOG2_QUANTUM, so the size of array is lookupMaxclass >> LOG2_QUANTUM
// 查询表 对容量<= 4K的规格,按照16B 为增量 做个详细的划分。 idx=>index 有256个(4096>>4)
private final int[] size2idxTab;
由上述常量和变量可看出来,大量的使用了 log2() 来规划这些值。 刚开始阅读的时候,会不习惯,为什么要使用log2() 来表示这些值呢? 用原值不行吗?
我个人觉得大致的原因: 使用log2()来计算,可以化整 ,且保证最终移位回来还是 power of two
例如:某个用户设置一页的大小为8193,那么log2(8193) = 13, 最终移位13后 还是 8192 (1<<13) 。
构造方法
protected SizeClasses(int pageSize, int pageShifts, int chunkSize, int directMemoryCacheAlignment) {
// 每一页的大小(默认为 8192 8KB)
this.pageSize = pageSize;
// 1左移多少位 为8192 (默认为 13)
this.pageShifts = pageShifts;
// 这个chunk的大小 (默认为16777216,即16mb)
this.chunkSize = chunkSize;
// 主要是对于Huge这种直接分配的 类型的 数据 将它 对齐为 directMemoryCacheAlignment的倍数(可以忽略该变量,对后面的逻辑没有影响)
this.directMemoryCacheAlignment = directMemoryCacheAlignment;
// 16mb => 16,777,216 => 0000 0001 0000 0000 0000 0000 0000 0000
// log2(chunkSize) = 31 - 7 = 24
// 24 + 1 - 4 = 21 => group
int group = log2(chunkSize) + 1 - LOG2_QUANTUM;
//generate size classes
//[index, log2Group, log2Delta, nDelta, isMultiPageSize, isSubPage, log2DeltaLookup]
// 21 << 2 => 21*4=84
// sizeClasses = {84}{7}
sizeClasses = new short[group << LOG2_SIZE_CLASS_GROUP][7];
// 生成sizeClasses表数据
nSizes = sizeClasses();
//generate lookup table
// 下面都是生成 查询表的数据了
//sizeIdx2sizeTab size 关联 id的表
sizeIdx2sizeTab = new int[nSizes];
//pageIdx2sizeTab size是页的整数倍的表 (维护关系 id和size)
pageIdx2sizeTab = new int[nPSizes];
// 填充sizeIdx2sizeTab 和 pageIdx2sizeTab 这两张表的数据
idx2SizeTab(sizeIdx2sizeTab, pageIdx2sizeTab);
// 4096 >> 4 = 256
size2idxTab = new int[lookupMaxSize >> LOG2_QUANTUM];
size2idxTab(size2idxTab);
}
sizeClasses
// 生成 sizeClasses二维数组表
private int sizeClasses() {
int normalMaxSize = -1;
int index = 0;
int size = 0;
// log2Group 和 log2Delta 初始值 4
int log2Group = LOG2_QUANTUM;
int log2Delta = LOG2_QUANTUM;
// ndeltaLimit = 1<<2 = 4 最大增量为4 也就是限制 1个group中有4个size
int ndeltaLimit = 1 << LOG2_SIZE_CLASS_GROUP;
//First small group, nDelta start at 0. 第一个 small组 nDetla从0开始
//first size class is 1 << LOG2_QUANTUM 第一个 size 是 1<<4 = 16
int nDelta = 0;
// 生成 第一组的值,放入sizeClasses二维数组中
// nDelta 从0开始 不超过4 也就是一组有4个size
while (nDelta < ndeltaLimit) {
// 给sizeClasses二维数组赋值
// 第一组 index = 0 log2Group=4 log2Delta=4 nDelta=0 根据这几个值计算并返回size值
size = sizeClass(index++, log2Group, log2Delta, nDelta++);
}
// 生成 第二组及后面组的值
// log2Group = 4 +2 = 6 log2Group开始为6
log2Group += LOG2_SIZE_CLASS_GROUP;
//All remaining groups, nDelta start at 1.
//剩下每组的 nDelta从1开始
while (size < chunkSize) {
nDelta = 1;
while (nDelta <= ndeltaLimit && size < chunkSize) {
size = sizeClass(index++, log2Group, log2Delta, nDelta++);
// 记录最大的size值
normalMaxSize = size;
}
log2Group++;
log2Delta++;
}
// 断言下 最后生成的 size值是否符合 chunkSize
//chunkSize must be normalMaxSize
assert chunkSize == normalMaxSize;
//return number of size index
// 返回最大index 默认应该是 75
return index;
}
sizeClass
private int sizeClass(int index, int log2Group, int log2Delta, int nDelta) {
short isMultiPageSize;
// log2Delta >= 13 的 其size都是页(8KB)的整倍数
if (log2Delta >= pageShifts) {
isMultiPageSize = yes;
} else {
// pageSize = 8192 8KB
int pageSize = 1 << pageShifts;
// 计算size的公式
int size = (1 << log2Group) + (1 << log2Delta) * nDelta;
// 判断是不是 页的整数倍
isMultiPageSize = size == size / pageSize * pageSize? yes : no;
}
// 计算 log2(nDelta)
int log2Ndelta = nDelta == 0? 0 : log2(nDelta);
//remove 表示 当 size不是 2的次方的时候 为 yes
// 当 size是 2的次方的时候 为no
// 该值只是当 size >= MAX_LOOKUP_SIZE(4096) 时使用
byte remove = 1 << log2Ndelta < nDelta? yes : no;
// 实际上就是求 log2(size)的 只不过这样更方便而已 当nDelta=4的时候满足
int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;
if (log2Size == log2Group) {
remove = yes;
}
// 16B 32B ... 1024B 1280B .. 8K ... 28K 13+2 = 15 32768=32KB
// pageShifts = log2(8K) = 13
// pageShifts + LOG2_SIZE_CLASS_GROUP = 13 + 2 = 15 为 log2(32K)
// 得出结论: 若 size < 32K 则是 subPage
short isSubpage = log2Size < pageShifts + LOG2_SIZE_CLASS_GROUP? yes : no;
// log2DeltaLookup 当size <= 4096(4K) 值为log2Delta
// 当size > 4096 时 值为0
int log2DeltaLookup = log2Size < LOG2_MAX_LOOKUP_SIZE ||
log2Size == LOG2_MAX_LOOKUP_SIZE && remove == no
? log2Delta : no;
// 生成行
short[] sz = {
(short) index, (short) log2Group, (short) log2Delta,
(short) nDelta, isMultiPageSize, isSubpage, (short) log2DeltaLookup
};
// 插入数组
sizeClasses[index] = sz;
int size = (1 << log2Group) + (nDelta << log2Delta);
// 判断该行是否是 页的整数倍 若是则 nPSizes+1
if (sz[PAGESIZE_IDX] == yes) {
nPSizes++;
}
// 判断该行是否是 subPage 若是则 nSubpages + 1 ,smallMaxSizeIdx记录最大的subPage的index
if (sz[SUBPAGE_IDX] == yes) {
nSubpages++;
smallMaxSizeIdx = index;
}
// lookupMaxSize 记录 <= 4096(4K) 最大的size值
if (sz[LOG2_DELTA_LOOKUP_IDX] != no) {
lookupMaxSize = size; // 最大4K
}
return size;
}
int log2Ndelta = nDelta == 0? 0 : log2(nDelta);
byte remove = 1 << log2Ndelta < nDelta? yes : no;
int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;
sizeClass
函数中 其中上述三行代码 刚开始 我看的时候 一脸懵逼, 完全不知道是什么意思,仔细研究后发现并不难。
-
首先 通过上一期我们可知道
size = (1 << log2Group) + (1 << log2Delta) * nDelta
该求size公式可转换成size = 2^(log2Delta) * (4 + nDelta)
-
int log2Ndelta = nDelta == 0? 0 : log2(nDelta);
这行代码实际上不难理解,就是为了计算出 log2(nDelta) 的值。
我们知道 求log2() 实际上 是向下取整, 比如 log2(5) = 2 , log2(3) = 1 等等 5 ,3这些数不是 POWER OF TWO 时,其 log2值 都是向下取整的。
-
byte remove = 1 << log2Ndelta < nDelta? yes : no;
remove 值 判断
1<< log2Ndelta < nDelta
就是为了看此值是否是2的倍数。 而我们清楚 除了第一组之外,nDelta取值范围为 [1,4] ,那么该区间范围内,只有4符合 POWER OF TWO, 也就是每组的最后一个规格。 -
int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;
我们知道 nDelta 第一组的取值范围为 [0,3], 除了第一组其它组取值范围为[1,4],
为了让 size 的结果是 POWER OF TWO (2的次方数), 那么 nDelta 只能取 0 或 4 。
当nDelta = 0 时 也就是只有第一组的第一个规格满足,此时
size = 1<<log2Group
当nDelta = 4 时,也就是除了第一组外 其他组的最后一个规格满足,此时
size = 1<<(log2Group + 1)
idx2SizeTab
该方法主要就是用来生成 sizeIdx2sizeTab
表,与pageIdx2sizeTab
表的。
很简单,没什么可说的。 生成的对应表详细信息, 可以在上一期查看。
private void idx2SizeTab(int[] sizeIdx2sizeTab, int[] pageIdx2sizeTab) {
int pageIdx = 0;
// 循环遍历总表
for (int i = 0; i < nSizes; i++) {
short[] sizeClass = sizeClasses[i];
int log2Group = sizeClass[LOG2GROUP_IDX];
int log2Delta = sizeClass[LOG2DELTA_IDX];
int nDelta = sizeClass[NDELTA_IDX];
// 计算该行 sizeClass 的 size值
int size = (1 << log2Group) + (nDelta << log2Delta);
// 填充sizeIdx2sizeTab表
sizeIdx2sizeTab[i] = size;
// 如果 isMultiPageSize 是 yes 也就是说 该sizeClass的size值是 页的整数倍的时候 加入pageIdx2sizeTab表中
if (sizeClass[PAGESIZE_IDX] == yes) {
pageIdx2sizeTab[pageIdx++] = size;
}
}
}
size2idxTab
生成的size2idxTab
表 可参考上一期。
private void size2idxTab(int[] size2idxTab) {
int idx = 0;
int size = 0;
// 遍历每一个 小于 4096 的规格
for (int i = 0; size <= lookupMaxSize; i++) {
// 获取 log2Delta
int log2Delta = sizeClasses[i][LOG2DELTA_IDX];
// 该代码可理解成 2^log2Delta/16
// 目的是 求出 该规格 可以按照16划分多少份
int times = 1 << log2Delta - LOG2_QUANTUM;
// size <= 4096
// 生成详细的 以16为增量的规格值
while (size <= lookupMaxSize && times-- > 0) {
size2idxTab[idx++] = i;
size = idx + 1 << LOG2_QUANTUM;
}
}
}
size2SizeIdx
该方法 是为了求出接近 用户申请内存大小 的内存规格下标。
当申请内存 <= 4096 时,直接从 size2idxTab
表中获取
当申请内存 > 4096 时,需要计算 group(申请内存所属组的第一个规格的index)+mod(该申请内存在所属组的偏移量)
例如: 用户申请大小为14 ,则返回数组索引值为 0。
public int size2SizeIdx(int size) {
if (size == 0) { // 当请求申请的size为0时, 则返回0
return 0;
}
if (size > chunkSize) { //当请求申请的size大于chunkSize时, 返回最大索引 也就是chunkSize的index
return nSizes;
}
// 当前偏移量大于0时,默认该条件不会成立
if (directMemoryCacheAlignment > 0) {
size = alignSize(size);
}
// 当size <= 4K 时 从size2idxTab 细粒度表中 直接获取index
if (size <= lookupMaxSize) {
return size2idxTab[size - 1 >> LOG2_QUANTUM];
}
// 来到这 就是 size > 4K 的时候
// 该表达式是为了将申请内存向上取整(该size的移位),得出该size所属组的移位。
// -1 是为了避免申请内存大小刚好是 POWER OF TWO(每组的最后一个规格),这样可以让每组的最后一个规格移位与同组其它规格移位相同。
// 最终 1<< 移位 = 当前组最后一个规格内存块的size
// 例子1: size = 16KB 2B0100 0000 0000 0000
// (size << 1) - 1 = 2B0100 0000 0000 00000 - 1
// = 2B0011 1111 1111 11111
// log2((size<<1)-1) = 14
// 例子2: size = 13KB 2B0011 0100 0000 0000
// (size << 1) - 1 = 2B0011 0100 0000 00000 -1
// = 2B0011 0011 1111 11111
// log2((size<<1)-1) = 14
int x = log2((size << 1) - 1);
/**
* 1. 从上得出x是每组最后一个内存块的size的移位。
*
* 2. 已知 log2Group = log2Delta + LOG2_SIZE_CLASS_GROUP
* 除了第一组外,每组的最后一个内存块的 nDelta = 4
* 将上面两个已知条件代入求size的公式可得:
* lastSize = 1 << (log2Group + 1)
* 两边同时 取log2 得:
* x = log2Group + 1
*/
// x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
// 将 x = log2Group + 1 代入后得:
// log2Group < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM
// log2Group < 6 时 shift = 0 (第一组)
// log2Group >= 6 时 shift = log2Group - 5 (第二组及以后)
// 最终shift 可以通过后面得表达式计算出 该组第一个内存块得index索引
int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);
// 该表达式就是计算 该组得第一个内存块的 index
int group = shift << LOG2_SIZE_CLASS_GROUP;
// 下面时计算 mod值 ,最终确定要分配得内存块是属于该组得第几个
// 计算log2Delta
// 当第0组时 log2Delta = 4
// 第一组及以后 log2Delta = log2Group - 2
int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;
// 计算掩码
// size = (1<<log2Delta) * (4 + nDelta)
// index = group + mod
// group 是该组得第一个内存块索引
// mod 是偏移位置 也即是 nDelta - 1
// size=(LOG2_SIZE_CLASS_GROUP + nDelta -1 + 1)* (1<<log2Delta)
// 再进一步转换 将1提出来
//size=(LOG2_SIZE_CLASS_GROUP + nDelta -1)*(1<<log2Delta)+(1<<log2Delta)
//LOG2_SIZE_CLASS_GROUP + nDelta-1=(size-(1<<log2Delta))/(1<<log2Delta)
int deltaInverseMask = -1 << log2Delta;
// size - 1 是为了申请内存等于内存块size时避免分配到下一个内存块size中
// & deltaInverseMask 可以理解为 减去 1<<log2Delta
// >> log2Delta 可以理解成 除以 1<<log2Delta
// 此时 就得到 LOG2_SIZE_CLASS_GROUP + nDelta-1
//mod = LOG2_SIZE_CLASS_GROUP + nDelta-1 & (1<< LOG2_SIZE_CLASS_GROUP)-1
//最终 mod = nDelta - 1
int mod = (size - 1 & deltaInverseMask) >> log2Delta &
(1 << LOG2_SIZE_CLASS_GROUP) - 1;
//28 + 3 = 31
return group + mod;
}
该方法可能看上去比较复杂,晦涩难懂,但是仔细跟着代入公式算一遍,其实很好理解。
page2pageIdxCompute
该方法主要目的就是 根据申请得 页数,来计算该 页数 在 pageIdx2sizeTab
表中得 pageIdx
该方法与 size2SizeIdx实际上计算思想相同,都是使用 group + mod。
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;
if (floor && pageIdx2sizeTab[pageIdx] > pages << pageShifts) {
pageIdx--;
}
return pageIdx;
}