Netty源碼分析:PoolSubpage
在上篇介紹Netty源碼分析:PoolChunk的博文中,我們分析了allocateSubpage方法(如下)的前半部分,後半部分是藉助於PoolSubpage來完成的。這篇博文就介紹下PoolSubpage這個類。
private long allocateSubpage(int normCapacity) {
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;
int subpageIdx = subpageIdx(id);
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null) {
subpage = new PoolSubpage<T>(this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(normCapacity);
}
return subpage.allocate();
}
1、PoolSubpage類的屬性和構造函數
final class PoolSubpage<T> {
final PoolChunk<T> chunk;//用來表示該Page屬於哪個Chunk
private final int memoryMapIdx;//用來表示該Page在Chunk.memoryMap中的索引
// 當前Page在chunk.memoryMap的偏移量
private final int runOffset;
private final int pageSize;//Page的大小,默認爲8192
/*
long 類型的數組bitmap用來表示Page中存儲區域的使用狀態,
數組中每個long的每一位表示一個塊存儲區域的佔用情況:0表示未佔用,1表示佔用。
例如:對於一個4K的Page來說如果這個Page用來分配1K的存儲與區,
那麼long數組中就只有一個long類型的元素且這個數值的低4危用來指示4個存儲區域的佔用情況。
*/
private final long[] bitmap;
//PoolSubpage本身設計爲一個鏈表結構
PoolSubpage<T> prev;
PoolSubpage<T> next;
boolean doNotDestroy;
int elemSize;//塊的大小
private int maxNumElems;//page按elemSize大小分得的塊個數
private int bitmapLength;//bitmap數組實際會用到的長度,等於pageSize/elemSize/64
// 下一個可用的位置
private int nextAvail;
// 可用的段數量
private int numAvail;
PoolSubpage(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,這裏的16指的是塊的最小值,64是long類型的所佔的bit數。
init(elemSize);
}
如在Netty源碼分析:PoolChunk的博文中介紹的:
對於小於一個Page的內存,Netty在Page中完成分配。每個Page會被切分成大小相同的多個存儲塊,存儲塊的大小由第一次申請的內存塊大小決定。對於Page的大小爲4K,第一次申請的時1K,則這個Page就會被分成4個存儲塊。
一個Page只能用於分配與第一次申請時大小相同的內存,例如,一個4K的Page,如果第一次分配了1K的內存,那麼後面這個Page就只能繼續分配1K的內存,如果有一個申請2K內存的請求,就需要在一個新的Page中進行分配。
Page中存儲區域的使用狀態通過一個long數組來維護,數組中每個long的每一位表示一個塊存儲區域的佔用情況:0表示未佔用,1表示佔用。例如:對於一個4K的Page來說如果這個Page用來分配1K的存儲與區,那麼long數組中就只有一個long類型的元素且這個數值的低4危用來指示4個存儲區域的佔用情況。
回到如上所示的關於PoolSubpage的字段和構造函數的代碼中:
在PoolSubpage的實現中,使用的是字段private final long[] bitmap;
來記錄Page的使用狀態,其中bitmap數組的最大長度爲:pageSize / 16 / 64,這裏的16指的是塊的最小值,64是long類型的所佔的bit數。
而存儲塊的大小使用的是字段elemSize來記錄,當第一次在這個page上申請小於pageSize的內存時將調用如下的init函數來記錄相關的塊信息,例如:塊的大小、塊的個數、初始化bitmap等。
void init(int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;//等價於bitmapLength = maxNumElems / 64;64爲long類型所佔的bit數
if ((maxNumElems & 63) != 0) { //如果塊的個數不是64的整倍數,則加 1
bitmapLength ++;
}
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
addToPool();
}
繼續看addToPool()方法
該方法的功能爲:將當前的Page加入到PoolArena所持有的PoolSubpage<T>[] tinySubpagePools;
或PoolSubpage<T>[] smallSubpagePools
數組中,爲什麼要加入到這裏面來呢??
這是因爲PoolSubpage[] tinySubpagePools,數組默認長度爲32(512 >>4),這裏面存儲的Page是專門用來分配小內存tiny(小於512),smallSubpagePools數組中存儲的Page則是用來分配small(大於等於512小於pageSize)內存的。
private void addToPool() {
PoolSubpage<T> head = chunk.arena.findSubpagePoolHead(elemSize);
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
2、subpage.allocate()
/**
* Returns the bitmap index of the subpage allocation.
*/
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
//判斷此page是否還有“塊”可用,以及是否被銷燬了,如果沒有可用空間或者是被銷燬了則返回-1.
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;//此bit位此時應該爲0.
bitmap[q] |= 1L << r;//將bitmap[q]這個long型的數的第rbit置爲1,標識此“塊”已經被分配。
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
該函數主要邏輯爲:
1、首先通過getNextAvail()方法來得到此Page中下一個可用“塊”的位置bitmapIdx。至於如何來得到,稍後將分析。
2、將bitmapIdx“可用塊”在bitmap中標識爲“已佔用”的狀態。具體如何來做的呢?
2.1)、根據q = bitmapIdx >>> 6
和r = bitmapIdx & 63
兩行代碼得到第bitmapIdx這個可用“內存塊”在bitmap標識數組中是第q個long元素且是第q個元素的第r爲來進行標識的。例如:假設bitmapIdx=66,則q=1,r=2,即是用bitmap[1]這個long類型數的第2個bit位來表示此“內存塊”的。
2.2)、利用assert (bitmap[q] >>> r & 1) == 0
判斷分配前bitmap[q]第r(bit)位一定是0,0:未佔用。由於馬上就將此“內存塊”分配出去,因此利用bitmap[q] |= 1L << r
將bitmap[q]第r(bit)位置爲1,1:佔用。
3、將page的可用“塊數”numAvail減一,減一之後如果結果爲0,則表示此Page的內存無可分配的了,因此,將其從Arena所持有的鏈表中移除。
下面來看下getNextAvail()方法是如何得到此Page中下一個可用“塊”的位置bitmapIdx的?
先猜測一下:對標識數組bitmap的每一個long元素的每一位進行判斷,看是否爲0,0表示爲佔用。
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {//page被第一次申請可用“塊”的時候nextAvail=0,會直接返回。表示直接用第0位內存塊
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
如果是第2、3…來申請時,則會調用如下的findNextAvail()來實現。下面的代碼比較簡單,和猜測的一樣,確實是通過按順序遍歷判斷標識數組bitmap的每一個long元素的每一個bit是否爲零來得到。不過下面的代碼有一點值得我們學習:將位操作應用的淋漓盡致。
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
//對標識數組bitmap中的每一個long元素進行判斷
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
if (~bits != 0) {//還存在至少1個bit位不爲1,1表示佔用
return findNextAvail0(i, bits);
}
}
return -1;
}
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6;
//對long類型的數的每一bit位進行判斷。
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) { //第j(bit)爲0,即爲佔用
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1;
}
return -1;
}
小結
分析完PoolSubpage這個類,我們需要了解3點:
1、PoolSubpage這個類的塊大小是由第一次申請的內存大小來決定的。
2、PoolSubpage這個類是通過long類型的數組bitmap來對PoolSubPage中的每個塊的使用情況進行標識的。
3、如果想在某個PoolSubpage分配一個小於pageSize的內存,則首先是通過按順序遍歷標識數組bitmap中每個long元素中的每一bit位中爲0的位置。