前面文章說了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實現原理》。
如果您覺得本文不錯,歡迎關注我的微信公眾號,系列文章持續更新中。您的關注是我堅持的動力!