共计 5421 个字符,预计需要花费 14 分钟才能阅读完成。
后面文章说了 PoolChunk 如何治理 Normal 内存块,本文分享 PoolSubpage 如何治理 Small 内存块。
源码剖析基于 Netty 4.1.52
内存治理算法
PoolSubpage 负责管理 Small 内存块。一个 PoolSubpage 中的内存块 size 都雷同,该 size 对应 SizeClasses#sizeClasses 表格的一个索引 index。
新创建的 PoolSubpage 都必须退出到 PoolArena#smallSubpagePools[index]链表中。
PoolArena#smallSubpagePools 是一个 PoolSubpage 数组,数组中每个元素都是一个 PoolSubpage 链表,PoolSubpage 之间能够通过 next,prev 组成链表。
感兴趣的同学能够参考《内存对齐类 SizeClasses》。
留神,Small 内存 size 并不一定小于 pageSize(默认为 8K)
默认 Small 内存 size <= 28672(28KB)
对于 Normal 内存块,Small 内存块,pageSize,可参考《PoolChunk 实现原理》。
PoolSubpage 实际上就是 PoolChunk 中的一个 Normal 内存块,大小为其治理的内存块 size 与 pageSize 最小公倍数。
PoolSubpage 应用位图的形式治理内存块。
PoolSubpage#bitmap 是一个 long 数组,其中每个 long 元素上每个 bit 位都能够代表一个内存块是否应用。
内存调配
调配 Small 内存块有两个步骤
- PoolChunk 中调配 PoolSubpage。
如果 PoolArena#smallSubpagePools 中曾经有对应的 PoolSubpage 缓冲,则不须要该步骤。
- PoolSubpage 上分配内存块
PoolChunk#allocateSubpage
private long allocateSubpage(int sizeIdx) {
// #1
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
synchronized (head) {
//allocate a new run
// #2
int runSize = calculateRunSize(sizeIdx);
//runSize must be multiples of pageSize
// #3
long runHandle = allocateRun(runSize);
if (runHandle < 0) {return -1;}
// #4
int runOffset = runOffset(runHandle);
int elemSize = arena.sizeIdx2size(sizeIdx);
PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
runSize(pageShifts, runHandle), elemSize);
subpages[runOffset] = subpage;
// #5
return subpage.allocate();}
}
#1
这里波及批改 PoolArena#smallSubpagePools 中的 PoolSubpage 链表,须要同步操作#2
计算内存块 size 和 pageSize 最小公倍数#3
调配一个 Normal 内存块,作为 PoolSubpage 的底层内存块,大小为 Small 内存块 size 和 pageSize 最小公倍数#4
构建 PoolSubpage
runOffset,即 Normal 内存块偏移量,也是该 PoolSubpage 在整个 Chunk 中的偏移量
elemSize,Small 内存块 size#5
在 subpage 上分配内存块
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int pageShifts, int runOffset, int runSize, int elemSize) {
// #1
this.chunk = chunk;
this.pageShifts = pageShifts;
this.runOffset = runOffset;
this.runSize = runSize;
this.elemSize = elemSize;
bitmap = new long[runSize >>> 6 + LOG2_QUANTUM]; // runSize / 64 / QUANTUM
init(head, elemSize);
}
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
if (elemSize != 0) {
// #2
maxNumElems = numAvail = runSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {bitmapLength ++;}
for (int i = 0; i < bitmapLength; i ++) {bitmap[i] = 0;
}
}
// #3
addToPool(head);
}
#1
bitmap 长度为 runSize / 64 / QUANTUM,从《内存对齐类 SizeClasses》能够看到,runSize 都是 2^LOG2_QUANTUM 的倍数。
#2
elemSize:每个内存块的大小
maxNumElems:内存块数量
bitmapLength:bitmap 应用的 long 元素个数,应用 bitmap 中一部分元素足以治理全副内存块。(maxNumElems & 63) != 0
,代表 maxNumElems 不能整除 64,所以 bitmapLength 要加 1,用于治理余下的内存块。#3
增加到 PoolSubpage 链表中
后面剖析《Netty 内存池与 PoolArena》中说过,在 PoolArena 中调配 Small 内存块时,首先会从 PoolArena#smallSubpagePools 中查找对应的 PoolSubpage。如果找到了,间接从该 PoolSubpage上分配内存。否则,调配一个 Normal 内存块,创立 PoolSubpage,再在下面分配内存块。
PoolSubpage#allocate
long allocate() {
// #1
if (numAvail == 0 || !doNotDestroy) {return -1;}
// #2
final int bitmapIdx = getNextAvail();
// #3
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
bitmap[q] |= 1L << r;
// #4
if (-- numAvail == 0) {removeFromPool();
}
// #5
return toHandle(bitmapIdx);
}
#1
没有可用内存块,调配失败。通常 PoolSubpage 调配实现后会从 PoolArena#smallSubpagePools 中移除,不再在该 PoolSubpage 上分配内存,所以个别不会呈现这种场景。#2
获取下一个可用内存块的 bit 下标#3
设置对应 bit 为 1,即已应用bitmapIdx >>> 6
,获取该内存块在 bitmap 数组中第 q 元素bitmapIdx & 63
,获取该内存块是 bitmap 数组中第 q 个元素的第 r 个 bit 位bitmap[q] |= 1L << r
,将 bitmap 数组中第 q 个元素的第 r 个 bit 位设置为 1,示意曾经应用#4
所有内存块已调配了,则将其从 PoolArena 中移除。#5
toHandle 转换为最终的 handle
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();}
nextAvail 为初始值或 free 时开释的值。
如果 nextAvail 存在,设置为不可用后间接返回该值。
如果不存在,调用 findNextAvail 查找下一个可用内存块。
private int findNextAvail() {final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// #1
for (int i = 0; i < bitmapLength; i ++) {long bits = bitmap[i];
if (~bits != 0) {return findNextAvail0(i, bits);
}
}
return -1;
}
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;
// #2
for (int j = 0; j < 64; j ++) {if ((bits & 1) == 0) {
int val = baseVal | j;
if (val < maxNumElems) {return val;} else {break;}
}
bits >>>= 1;
}
return -1;
}
#1
遍历 bitmap,~bits != 0
,示意存在一个 bit 位不为 1,即存在可用内存块。#2
遍历 64 个 bit 位,(bits & 1) == 0
,查看最低 bit 位是否为 0(可用),为 0 则返回 val。
val 等于 (i << 6) | j
,即i * 64 + j
,该 bit 位在 bitmap 中是第几个 bit 位。bits >>>= 1
,右移一位,解决下一个 bit 位。
内存开释
开释 Small 内存块可能有两个步骤
- 开释 PoolSubpage 的上内存块
- 如果 PoolSubpage 中的内存块已全副开释,则从 Chunk 中开释该 PoolSubpage,同时从 PoolArena#smallSubpagePools 移除它。
PoolSubpage#free
boolean free(PoolSubpage<T> head, int bitmapIdx) {if (elemSize == 0) {return true;}
// #1
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r;
setNextAvail(bitmapIdx);
// #2
if (numAvail ++ == 0) {addToPool(head);
return true;
}
// #3
if (numAvail != maxNumElems) {return true;} else {
// #4
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// #5
doNotDestroy = false;
removeFromPool();
return false;
}
}
#1
将对应 bit 位设置为能够应用 #2
在 PoolSubpage 的内存块全副被应用时,开释了某个内存块,这时重新加入到 PoolArena 中。#3
未齐全开释,即还存在已分配内存块,返回 true#4
逻辑到这里,是解决所有内存块曾经齐全开释的场景。
PoolArena#smallSubpagePools 链表组成双向链表,链表中只有 head 和以后 PoolSubpage 时,以后 PoolSubpage 的 prev,next 都指向 head。
这时以后PoolSubpage 是 PoolArena 中该链表最初一个 PoolSubpage,不开释该 PoolSubpage,以便下次申请内存时间接从该 PoolSubpage 上调配。#5
从 PoolArena 中移除,并返回 false,这时 PoolChunk 会将开释对应 Page 节点。
void free(long handle, int normCapacity, ByteBuffer nioBuffer) {if (isSubpage(handle)) {
// #1
int sizeIdx = arena.size2SizeIdx(normCapacity);
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
PoolSubpage<T> subpage = subpages[runOffset(handle)];
assert subpage != null && subpage.doNotDestroy;
synchronized (head) {
// #2
if (subpage.free(head, bitmapIdx(handle))) {
//the subpage is still used, do not free it
return;
}
}
}
// #3
...
}
#1
查找 head 节点,同步 #2
调用 subpage#free 开释 Small 内存块
如果 subpage#free 返回 false,将持续向下执行,这时会开释 PoolSubpage 整个内存块,否则,不开释 PoolSubpage 内存块。#3
开释 Normal 内存块,就是开释 PoolSubpage 整个内存块。该局部内容可参考《PoolChunk 实现原理》。
如果您感觉本文不错,欢送关注我的微信公众号,系列文章继续更新中。您的关注是我保持的能源!