技術文章第一時間送達!
多年之前,從C記憶體的手動管理上升到java的自動GC,是曆史的巨大進步。然而多年之後,netty的記憶體實作又曲線的回到了手動管理模式,正印證了馬克思哲學觀:社會總是在螺旋式前進的,沒有永遠的最好。的确,就記憶體管理而言,GC給程式員帶來的價值是不言而喻的,不僅大大的降低了程式員的負擔,而且也極大的減少了記憶體管理帶來的Crash困擾,不過也有很多情況,可能手動的記憶體管理更為合适。
接下去準備幾個篇幅對Netty的記憶體管理進行深入分析。
PoolChunk
為了能夠簡單的操作記憶體,必須保證每次配置設定到的記憶體時連續的。Netty中底層的記憶體配置設定和回收管理主要由PoolChunk實作,其内部維護一棵平衡二叉樹memoryMap,所有子節點管理的記憶體也屬于其父節點。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SO2IDN2ADOwcjY1kjMkFzNmdjYjhTOlZjZ4MWZyETZh9CX5IzLcdDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL1M3Lc9CX6MHc0RHaiojIsJye.png)
img
memoryMap
poolChunk預設由2048個page組成,一個page預設大小為8k,圖中節點的值為在數組memoryMap的下标。
1、如果需要配置設定大小8k的記憶體,則隻需要在第11層,找到第一個可用節點即可。
2、如果需要配置設定大小16k的記憶體,則隻需要在第10層,找到第一個可用節點即可。
3、如果節點1024存在一個已經被配置設定的子節點2048,則該節點不能被配置設定,如需要配置設定大小16k的記憶體,這個時候節點2048已被配置設定,節點2049未被配置設定,就不能直接配置設定節點1024,因為該節點目前隻剩下8k記憶體。
poolChunk内部會保證每次配置設定記憶體大小為8K*(2n),為了配置設定一個大小為chunkSize/(2k)的節點,需要在深度為k的層從左開始比對節點,那麼如何快速的配置設定到指定記憶體?
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數組中每個位置儲存的是該節點所在的層數,有什麼作用?對于節點512,其層數是9,則:
1、如果memoryMap[512] = 9,則表示其本身到下面所有的子節點都可以被配置設定;
2、如果memoryMap[512] = 10, 則表示節點512下有子節點已經配置設定過,則該節點不能直接被配置設定,而其子節點中的第10層還存在未配置設定的節點;
3、如果memoryMap[512] = 12 (即總層數 + 1), 可配置設定的深度已經大于總層數, 則表示該節點下的所有子節點都已經被配置設定。
下面看看如何向PoolChunk申請一段記憶體:
long allocate(int normCapacity) {
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
} else {
return allocateSubpage(normCapacity);
}
}
1、當需要配置設定的記憶體大于pageSize時,使用allocateRun實作記憶體配置設定。
2、否則使用方法allocateSubpage配置設定記憶體,在allocateSubpage實作中,會把一個page分割成多段,進行記憶體配置設定。
這裡先看看allocateRun是如何實作的:
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是處理過的值,如申請大小為1000的記憶體,實際申請的記憶體大小為1024。
2、d = maxOrder - (log2(normCapacity) - pageShifts) 可以确定需要在二叉樹的d層開始節點比對。
其中pageShifts預設值為13,為何是13?因為隻有當申請記憶體大小大于2^13(8192)時才會使用方法allocateRun配置設定記憶體。
3、方法allocateNode實作在二叉樹中進行節點比對,具體實作如下:
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、從根節點開始周遊,如果目前節點的
val<d
,則通過
id <<=1
比對下一層;
2、如果val > d,則表示存在子節點被配置設定的情況,而且剩餘節點的記憶體大小不夠,此時需要在兄弟節點上繼續查找;
3、配置設定成功的節點需要标記為不可用,防止被再次配置設定,在memoryMap對應位置更新為12;
4、配置設定節點完成後,其父節點的狀态也需要更新,并可能引起更上一層父節點的更新,實作如下:
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;
}
}
比如節點2048被配置設定出去,更新過程如下: