深入淺出Netty內存管理 PoolChunk

多年之前,從C內存的手動管理上升到java的自動GC,是歷史的巨大進步。然而多年之后,netty的內存實現又曲線的回到了手動管理模式,正印證了馬克思哲學觀:社會總是在螺旋式前進的,沒有永遠的最好。的確,就內存管理而言,GC給程序員帶來的價值是不言而喻的,不僅大大的降低了程序員的負擔,而且也極大的減少了內存管理帶來的Crash困擾,不過也有很多情況,可能手動的內存管理更為合適。

接下去準備幾個篇幅對Netty的內存管理進行深入分析。

PoolChunk

為了能夠簡單的操作內存,必須保證每次分配到的內存時連續的。Netty中底層的內存分配和回收管理主要由PoolChunk實現,其內部維護一棵平衡二叉樹memoryMap,所有子節點管理的內存也屬于其父節點。

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被分配出去,更新過程如下:

memoryMap節點更新

到目前為止,基于poolChunk的節點分配已經完成。

END。
我是占小狼。
在魔都艱苦奮斗,白天是上班族,晚上是知識服務工作者。
如果讀完覺得有收獲的話,記得關注和點贊哦。
非要打賞的話,我也是不會拒絕的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容