PoolSubPage
在PoolChunk分配內(nèi)存時推励,如果normCapacity & subpageOverflowMask = 0联予,也就是申請分配的內(nèi)存大小不到一個Page,則會申請分配SubPage。事實上早在PoolChunk構(gòu)造函數(shù)中就初始化了一個成員變量subpages陵珍,這是一個PoolSubPage的數(shù)組握爷,長度為2^maxOrder扑浸,默認2048聘芜,與Page二叉樹葉子結(jié)點數(shù)量一致。此外在PoolArena里也有2個PoolSubpage數(shù)組趋厉,它們被命名為tinySubpagePools和smallSubpagePools寨闹。
tinySubpage是指subpage大小在16B496B之間的數(shù)組,以16B的數(shù)量遞增君账,數(shù)組長度為512B/16B=32鼻忠;smallSubpage是指subpage大小在512B4MB之間的數(shù)組,以2的指數(shù)遞增杈绸,表示分別分配512KB,1MB,2MB,4MB大小的smallSubpage帖蔓,因此它的長度為4。
鋪墊完之后瞳脓,先看一下PoolSubPage初始化過程塑娇。
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];
init(head, elemSize);
}
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);
}
private void addToPool(PoolSubpage<T> head) {
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
顯然,構(gòu)造函數(shù)可以根據(jù)調(diào)用的方法數(shù)分成3個步驟
- 初始化chunk等成員變量劫侧。
- 計算最大poolSubpage數(shù)量埋酬。
- 添加poolSubpage到PoolArena的雙向鏈表中。
一個個步驟來講述
初始化chunk等成員變量
先介紹一下這里面涉及到的成員變量烧栋。head是一個特殊的PoolSubpage写妥,位于PoolArena中;chunk即是分配所在的PoolChunk审姓;memoryMap是Chunk中的滿二叉樹珍特;runOffset是此次分配相對于滿二叉樹數(shù)組的偏移量;pageSize是Chunk中的pageSize魔吐,默認8KB扎筒;elemSize是一個規(guī)整化后的結(jié)果莱找,在初次分配時,當(dāng)前page將按這個大小進行切分嗜桌,之后相同大小的PoolSubpage將組成雙向鏈表奥溺。bitmap是一個long類型數(shù)組,netty使用它來描述PoolSubpage雙向鏈表中的內(nèi)存分配情況骨宠,數(shù)組長度為pageSize >>> 10浮定,默認為8,這里需要解釋一下這個長度:由于一個Page默認大小為8KB层亿,而PoolSubpage大小最小為16B桦卒,因此最多可以分配8192 / 16 = 512個poolSubpage。由于long類型數(shù)字有64位棕所,所以最多需要8個long元素組成的數(shù)組來描述分配情況闸盔。
計算最大poolSubpage數(shù)量
maxNumElems表示最多可能擁有的poolSubpage數(shù)量悯辙,numAvial表示當(dāng)前還可分配的poolSubpage數(shù)量琳省。在未使用時,maxNumElems和numAvial相等躲撰,都為pageSize / elemSize针贬,相關(guān)邏輯可以參考上文biamap的描述。
bitmapLength表示bitmap數(shù)組的長度拢蛋,因為PoolSubpage并不都只有16B桦他,大一些的PoolSubpage不需要完整的8個long來表示。這個值通過將PoolSubpage除以代表long類型長度的64得到谆棱。如果maxNumElems & 63 != 0快压,表明maxNumElems不是64的整數(shù)倍,需要為這個余數(shù)單獨增加一個bitmap元素垃瞧。之后將所有bitmap元素都初始化為0.
添加poolSubpage到PoolArena的雙向鏈表中
PoolArena的2個PoolSubpage數(shù)組默認都會被設(shè)置為head蔫劣,head并不承載數(shù)據(jù),只作為一個哨兵節(jié)點存在个从。這里添加到PoolArena就是一個簡單的雙向鏈表操作脉幢,只需要注意每次插入PoolSubpage都是插入在head之后,也即頭插法嗦锐。在這之后PoolArena分配相同大小的內(nèi)存時嫌松,可以直接進行分配,而不必先初始化PoolChunk奕污,分配Page后再分配PoolSubpage萎羔。
PoolSubpage分配內(nèi)存
在介紹完P(guān)oolSubpage的初始化和一些成員變量后,開始分析它的內(nèi)存分配過程碳默。
private long allocateSubpage(int normCapacity) {
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
int d = maxOrder;
synchronized (head) {
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>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
return subpage.allocate();
}
}
分配過程可以分為5個步驟
- 根據(jù)分配的大小找到PoolArena中對應(yīng)的PoolSubpage數(shù)組
- 找到可分配的Page節(jié)點外驱,并在可用空間中減去Page節(jié)點的容量育灸。這一步驟與PoolChunk尋找Page節(jié)點完全一樣,不再贅述昵宇。
- 找到在PoolChunk中subpage對應(yīng)的索引值磅崭。
- 初始化subpage。
- 分配subpage級別的內(nèi)存瓦哎。
找到對應(yīng)PoolSubpage數(shù)組
前文說過砸喻,PoolArena中有2個PoolSubpage數(shù)組,數(shù)組中的每一個元素實際上都會形成一個雙向鏈表蒋譬,而鏈表頭結(jié)點就是head割岛。這里倒是有那么一點hashmap的樣子,但hashmap是通過對key取hash值再對數(shù)組長度取模來定位到數(shù)組下標犯助,而PoolSubpage則是根據(jù)將要分配的大小癣漆。
PoolSubpage<T> findSubpagePoolHead(int elemSize) {
int tableIdx;
PoolSubpage<T>[] table;
if (isTiny(elemSize)) { // < 512
tableIdx = elemSize >>> 4;
table = tinySubpagePools;
} else {
tableIdx = 0;
elemSize >>>= 10;
while (elemSize != 0) {
elemSize >>>= 1;
tableIdx ++;
}
table = smallSubpagePools;
}
return table[tableIdx];
}
當(dāng)elemSize小于512時,PoolArena將會去tinyPoolSubpage數(shù)組查找剂买,由于每個PoolSubpage以16B的容量遞增惠爽,所以這里將elemSize右移4即除以16來計算數(shù)組下標。同理當(dāng)elemSize大于等于512時瞬哼,先右移10婚肆,若elemSize等于0,則說明elemSize等于512坐慰,定位到0號元素较性,否則逐步右移,意味著elemSize實際容量比預(yù)期要翻倍结胀。定位到數(shù)組元素后赞咙,返回head節(jié)點。
在步驟2開始前糟港,對head節(jié)點加了互斥鎖攀操,
找到subpage對應(yīng)的索引值
這里就是利用上面查找到的Page節(jié)點在Page二叉樹中的下標值menoryMapIdx與maxSubpageAllocs做異或運算,maxSubpageAllocs在數(shù)值上與Page二叉樹葉子節(jié)點數(shù)相同着逐。由于maxSubpageAllocs是2的冪次方崔赌,默認2048,轉(zhuǎn)化為二進制是1000,0000,0000耸别。而memoryMapIdx也是從2048開始到4095健芭,轉(zhuǎn)化為二進制是從1000,0000,0000到1111,1111,1111,兩者做異或運算時由于首位都為1秀姐,則結(jié)果范圍恰好一一在0-2047內(nèi)慈迈,沒有沖突。精妙的位運算運用。
private int subpageIdx(int memoryMapIdx) {
return memoryMapIdx ^ maxSubpageAllocs;
}
初始化subpage
在構(gòu)造函數(shù)中有一個參數(shù)runOffset痒留,在前文帶過了谴麦,這里再深入一下它是如何計算的。
private int runOffset(int id) {
int shift = id ^ 1 << depth(id)
return shift * runLength(id);
}
private int runLength(int id) {
return 1 << log2ChunkSize - depth(id);
}
depth(id)就是從PoolChunk中那個不變深度的滿二叉樹depthMap中取得對應(yīng)下標的深度伸头。這里還需要注意順序匾效,是先計算右移再異或,因此這里也是計算這個節(jié)點相對于完全二叉樹每一層最左邊的偏移量恤磷。而runLength(id)則是計算id這一層的每單位偏移量為多少面哼。兩者相乘,就是當(dāng)前相當(dāng)于id所在層最左側(cè)節(jié)點需要偏移多少量扫步。當(dāng)然由于這個方法只在PoolSubpage分配內(nèi)存時調(diào)用魔策,也可以當(dāng)做相對于2048葉子節(jié)點的偏移量。其余初始化流程不贅述河胎。
分配subpage級別的內(nèi)存
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;
bitmap[q] |= 1L << r;
if (-- numAvail == 0) {
removeFromPool();
}
return toHandle(bitmapIdx);
}
老規(guī)矩闯袒,忽略檢驗性代碼,分解成4個步驟
- 找出PoolSubpage中下一個可以分配的內(nèi)存塊游岳。
- 將內(nèi)存塊的bitmapIdx標識為已分配政敢。
- 如果該PoolSubpage不可分配了,從PoolArena中移除吭历。
- 將bitmapIdx放到返回值的高32位中堕仔。
先看如何找到內(nèi)存地址代表的bitmapIdx
private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}
在初始化時擂橘,nextAvial是為0的晌区,說明還未申請分配。當(dāng)?shù)谝淮畏峙渫旰笸ㄕ辏琻extAvial變成了-1朗若,說明已被分配,或許這個變量命名為thisAvial會更好理解?而如果已被分配昌罩,則進入findNextAvial方法尋找下一個可用點內(nèi)存塊哭懈。
findNextAvial則是根據(jù)bitmap來尋找可用內(nèi)存塊。"~bits != 0"表示如果該內(nèi)存塊按位翻轉(zhuǎn)后不為0茎用,說明原本存在為0即未分配的節(jié)點遣总,則進入該內(nèi)存塊尋找指定位置。
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;
}
這里只要謹記參數(shù)i是bitmap數(shù)組中第i個元素轨功,bits表示該元素的64位二進制旭斥。因此baseVal含義是把bitmap數(shù)組第i號元素左移6位。循環(huán)的目的是將bits不斷右移古涧,找到第一個不為0的二進制位垂券。當(dāng)找到時,將baseVal與j做或運算羡滑。這里也可以看出為什么baseVal左移6位菇爪,因為j最多只到63算芯,剛好占用6個二進制位,兩者不會有所沖突凳宙。
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;
}
找到bitmapIdx后熙揍,進入步驟2:通過將bitmapIdx右移6位,得到原本的i氏涩,與63做&運算诈嘿,得到j(luò),將bitmap[i]原本的值與j做或運算削葱,表示將bitmap數(shù)組第i號元素的第j位內(nèi)存塊標識為已完成分配奖亚。
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;
分配完一個內(nèi)存塊后,將numAvial自減并順便做了一個判斷析砸,numAvial為0意味著這個PoolSubpage不能再分配內(nèi)存塊了昔字,所以將其從PoolArena中去掉。這里也是一個簡單的雙鏈表操作首繁,不多描述作郭。
private void removeFromPool() {
prev.next = next;
next.prev = prev;
next = null;
prev = null;
}
最后則是將bitmapIdx放到返回值的高32位中。
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
這里可以結(jié)合PoolChunk的allocateRun方法的返回值一起思考:netty需要將2個返回值handle統(tǒng)一弦疮,所以將handle用long類型表示夹攒,其中l(wèi)ong類型的低32位表示PoolSubpage所屬的Page的下標,高32位中的7-10位表示bitmap數(shù)組中的元素號胁塞,低6位表示bitmap元素即一個long類型的實際分配情況咏尝。
初始化ByteBuf
在PoolChunk的allocate方法中,返回了handle后最終會進行Bytebuf的初始化啸罢,相關(guān)方法如下编检。
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
int memoryMapIdx = memoryMapIdx(handle);
int bitmapIdx = bitmapIdx(handle);
if (bitmapIdx == 0) {
byte val = value(memoryMapIdx);
buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
} else {
initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
}
}
memoryMapIdx(handle)和bitmapIdx(handle)分別取handle的后32位和前32位,結(jié)合前文說的handle的含義扰才,不難理解這2個方法允懂。
然后進入了bitmapIdx==0的判斷,如果條件成立衩匣,說明需要分配Page級別的Bytebuf蕾总,否則分配Subpage級別的Bytebuf。
兩種分支都是根據(jù)handle計算offset琅捏,最終調(diào)用buf.init方法將傳入的參數(shù)都保存到成員變量中生百。