PoolChunk負責內存的分配與釋放,其內部最小的分配單元爲page,page的默認大小爲8k。如果我們申請很多小塊內存時,都按照page來分配,那麼資源浪費可不是一點半點。針對這個問題,netty將page拆成了更小的內存塊element,但超出了PoolChunk的負責範圍,此時netty使用PoolSubpage來解決這個問題。
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
init(head, elemSize);
}
無非是賦值,我們來看下bitmap數組大小爲什麼是pageSize>>10;從註釋中可以看出,16B是elementSize最小值,64是long的位數,也就是說用bitmap的位個數可以滿足分配最多element時的數目。bitmap大小爲什麼不使用pageSize/elemenSize/64?因爲申請最大空間後,對於後面可回收後重新分配該對象,不需要重新開闢空間,只需重新調用init()賦值。
head是之前調用arena.findSubpagePoolHead(normCapacity)的返回值。可以從函數調用中分析出,該方法要不是在構造函數中調用,或者在subpage回收後重新分配時調用。
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
addToPool(head);
}
先把標誌位doNoDestroy設爲true,表示該page在使用中,不能被清除。然後賦值。計算出實際使用的bitmapLength大小。並把標誌位清零(因爲之前可能用過有垃圾數據,僅需把當前要用的部分清零)。
chunk在分配空間時,大小8k以下的空間交給subPage管理,然而chunk併爲將subPage暴露給外面,於是subPage通過addToPool()方法,將自己加入到chunk.arean的pool中。
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
僅僅是鏈表插入操作,將當前節點插入到head之後。
回到chunk中分配小於8k以下部分的方法中
int subpageIdx = subpageIdx(id);
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
我們來看下subpage.allocate();
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
bitmap[q] |= 1L << r;
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
如果此時沒有可分配的element,那麼直接返回。getNetxAvail()無非找到當前page中可分配的段的下標,然後在bitmap上標記該段被使用。如果該page所有可用element分配完了後,將這個subpage從pool中刪除。
我們可以看到這裏在subPage這個數據結構上進行“管理分配”,返回一個element的index。toHandle操作無非在返回的long上標記可用空間。
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
一個long,其中低32位用來表示memoryMapIdx,無非是執行分配的具體page,高32位中的低6位表示bitmap,即定位到當前page中可分配的段。
我們來看下getNextAvail方法。
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
直接從成員中取出下一個可用的位置,如果nextAvail>=0,要麼是剛初始化,或者是有element重新回收未分配的,直接返回,並把其標記爲-1,否則繼續調用findNextAvail。
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
if (~bits != 0) {
return findNextAvail0(i, bits);
}
}
return -1;
}
沒有明確指定nextAvail位置時,於是從頭到尾查。先定位到bitmaps中的哪個long沒有分配完。
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;
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;
}
於是找那個long上的哪個位未分配,於是構造val返回。val就是最低6位表示該long上的哪個字節,其他位即val >> 6後記錄的是哪個long,即bitmap的下標。其實val就是bitmapIdx,它定位到具體的某個element。
關於上一篇遺留問題,總結一下。
poolChunk的allocate函數返回一個long類型的handle,其中當handle<Integer.MAX_VALUE時,它表示chunk的節點id,當handle>Integer.MAX_VALUE,他分配的是一個Subpage,節點id=memoryMapIdx, 且能得到Subpage的bitmapIdx。
再來看下poolChunk對應的小於8k的free過程。
int bitmapIdx = bitmapIdx(handle);
if (bitmapIdx != 0) { // free a subpage
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
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.
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
}
先根據handle取到bitmapIdx,再取出對應的subpage,找到根據elemSize取出head,加鎖(應爲涉及到head爲頭的鏈表的改變),再調用subpage.free。
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r;
setNextAvail(bitmapIdx);
if (numAvail ++ == 0) {
addToPool(head);
return true;
}
if (numAvail != maxNumElems) {
return true;
} else {
// Subpage not in use (numAvail == maxNumElems)
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;
removeFromPool();
return false;
}
}
無非在bitmap上標記爲1可用,然後將當前bitmapIdx設置到nextAvail,如果當前subpage的可用大小爲0,那麼把可用大小加1,並且添加到pool中,即head爲頭的雙向鏈表中。如果添加後numAvail==maxNumElems說明該page上所有element都未分配。如果該subpage是鏈上的唯一一塊那麼不處理(這樣做儘可能的保證arena分配小內存時能直接從pool中取,而不用再到chunk中去獲取),否則將其從arean的pool的鏈表中剔除。連着寫了兩篇,netty內存池的結構越來越清晰了,當然問題還有不少,比如分配大小大於一整個chunk後如何處理,subpage的elemSize存在很多可能值,handle具體用處。一步步探索吧。