多年之前即寡,從C內(nèi)存的手動(dòng)管理上升到j(luò)ava的自動(dòng)GC江醇,是歷史的巨大進(jìn)步告喊。然而多年之后闪檬,netty的內(nèi)存實(shí)現(xiàn)又曲線的回到了手動(dòng)管理模式晨抡,正印證了馬克思哲學(xué)觀:社會(huì)總是在螺旋式前進(jìn)的氛悬,沒(méi)有永遠(yuǎn)的最好。的確耘柱,就內(nèi)存管理而言如捅,GC給程序員帶來(lái)的價(jià)值是不言而喻的,不僅大大的降低了程序員的負(fù)擔(dān)调煎,而且也極大的減少了內(nèi)存管理帶來(lái)的Crash困擾镜遣,不過(guò)也有很多情況,可能手動(dòng)的內(nèi)存管理更為合適士袄。
接下去準(zhǔn)備幾個(gè)篇幅對(duì)Netty的內(nèi)存管理進(jìn)行深入分析悲关。
PoolChunk
為了能夠簡(jiǎn)單的操作內(nèi)存,必須保證每次分配到的內(nèi)存時(shí)連續(xù)的娄柳。Netty中底層的內(nèi)存分配和回收管理主要由PoolChunk實(shí)現(xiàn)寓辱,其內(nèi)部維護(hù)一棵平衡二叉樹memoryMap,所有子節(jié)點(diǎn)管理的內(nèi)存也屬于其父節(jié)點(diǎn)赤拒。
poolChunk默認(rèn)由2048個(gè)page組成秫筏,一個(gè)page默認(rèn)大小為8k,圖中節(jié)點(diǎn)的值為在數(shù)組memoryMap的下標(biāo)挎挖。
1这敬、如果需要分配大小8k的內(nèi)存,則只需要在第11層蕉朵,找到第一個(gè)可用節(jié)點(diǎn)即可崔涂。
2、如果需要分配大小16k的內(nèi)存始衅,則只需要在第10層冷蚂,找到第一個(gè)可用節(jié)點(diǎn)即可缭保。
3、如果節(jié)點(diǎn)1024存在一個(gè)已經(jīng)被分配的子節(jié)點(diǎn)2048帝雇,則該節(jié)點(diǎn)不能被分配涮俄,如需要分配大小16k的內(nèi)存,這個(gè)時(shí)候節(jié)點(diǎn)2048已被分配尸闸,節(jié)點(diǎn)2049未被分配彻亲,就不能直接分配節(jié)點(diǎn)1024,因?yàn)樵摴?jié)點(diǎn)目前只剩下8k內(nèi)存吮廉。
poolChunk內(nèi)部會(huì)保證每次分配內(nèi)存大小為8K*(2n)苞尝,為了分配一個(gè)大小為chunkSize/(2k)的節(jié)點(diǎn),需要在深度為k的層從左開始匹配節(jié)點(diǎn)宦芦,那么如何快速的分配到指定內(nèi)存宙址?
memoryMap初始化:
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p) {
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
}
}
memoryMap數(shù)組中每個(gè)位置保存的是該節(jié)點(diǎn)所在的層數(shù),有什么作用调卑?對(duì)于節(jié)點(diǎn)512抡砂,其層數(shù)是9,則:
1恬涧、如果memoryMap[512] = 9注益,則表示其本身到下面所有的子節(jié)點(diǎn)都可以被分配;
2溯捆、如果memoryMap[512] = 10丑搔, 則表示節(jié)點(diǎn)512下有子節(jié)點(diǎn)已經(jīng)分配過(guò),則該節(jié)點(diǎn)不能直接被分配提揍,而其子節(jié)點(diǎn)中的第10層還存在未分配的節(jié)點(diǎn);
3啤月、如果memoryMap[512] = 12 (即總層數(shù) + 1), 可分配的深度已經(jīng)大于總層數(shù), 則表示該節(jié)點(diǎn)下的所有子節(jié)點(diǎn)都已經(jīng)被分配。
下面看看如何向PoolChunk申請(qǐng)一段內(nèi)存:
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
1劳跃、當(dāng)需要分配的內(nèi)存大于pageSize時(shí)谎仲,使用allocateRun實(shí)現(xiàn)內(nèi)存分配。
2刨仑、否則使用方法allocateSubpage分配內(nèi)存强重,在allocateSubpage實(shí)現(xiàn)中,會(huì)把一個(gè)page分割成多段贸人,進(jìn)行內(nèi)存分配。
這里先看看allocateRun是如何實(shí)現(xiàn)的:
private long allocateRun(int normCapacity) {
int d = maxOrder - (log2(normCapacity) - pageShifts);
int id = allocateNode(d);
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
1佃声、normCapacity是處理過(guò)的值艺智,如申請(qǐng)大小為1000的內(nèi)存,實(shí)際申請(qǐng)的內(nèi)存大小為1024圾亏。
2十拣、d = maxOrder - (log2(normCapacity) - pageShifts) 可以確定需要在二叉樹的d層開始節(jié)點(diǎn)匹配封拧。
其中pageShifts默認(rèn)值為13,為何是13夭问?因?yàn)橹挥挟?dāng)申請(qǐng)內(nèi)存大小大于2^13(8192)時(shí)才會(huì)使用方法allocateRun分配內(nèi)存泽西。
3、方法allocateNode實(shí)現(xiàn)在二叉樹中進(jìn)行節(jié)點(diǎn)匹配缰趋,具體實(shí)現(xiàn)如下:
private int allocateNode(int d) {
int id = 1;
int initial = - (1 << d);
//value(id)=memoryMap[id]
byte val = value(id);
if (val > d) { // unusable
return -1;
}
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id);
if (val > d) {
id ^= 1;
val = value(id);
}
}
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // mark as unusable
updateParentsAlloc(id);
return id;
}
1捧杉、從根節(jié)點(diǎn)開始遍歷,如果當(dāng)前節(jié)點(diǎn)的val<d秘血,則通過(guò)id <<=1匹配下一層味抖;
2、如果val > d灰粮,則表示存在子節(jié)點(diǎn)被分配的情況仔涩,而且剩余節(jié)點(diǎn)的內(nèi)存大小不夠,此時(shí)需要在兄弟節(jié)點(diǎn)上繼續(xù)查找粘舟;
3熔脂、分配成功的節(jié)點(diǎn)需要標(biāo)記為不可用,防止被再次分配柑肴,在memoryMap對(duì)應(yīng)位置更新為12霞揉;
4、分配節(jié)點(diǎn)完成后嘉抒,其父節(jié)點(diǎn)的狀態(tài)也需要更新零聚,并可能引起更上一層父節(jié)點(diǎn)的更新,實(shí)現(xiàn)如下:
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}
比如節(jié)點(diǎn)2048被分配出去些侍,更新過(guò)程如下:
到目前為止隶症,基于poolChunk的節(jié)點(diǎn)分配已經(jīng)完成。
END岗宣。
我是占小狼蚂会。
在魔都艱苦奮斗,白天是上班族耗式,晚上是知識(shí)服務(wù)工作者胁住。
如果讀完覺(jué)得有收獲的話,記得關(guān)注和點(diǎn)贊哦刊咳。
非要打賞的話彪见,我也是不會(huì)拒絕的。