Netty15# 池化內存Normal類型內存分配

前言

Netty所謂的池化就是先申請了一塊大內存,後面需要分配的時候就來我這裏分就完了。以堆外直接內存分配爲例,Netty以Chunk爲單位16M申請了一塊連續內存,這麼一大塊內存是以平衡二叉樹的形式組織起來的。分配的時候就從這顆樹上找合適的節點。池化內存的分配是Netty的最爲核心部分,這塊的代碼很多位運算,不太容易看懂,讀的時候需要邊調試邊分析。

平衡二叉樹

Normal類型的組織,Netty使用平衡二叉樹將申請到的Chunk塊組織起來,如下圖所示,並使用數組將整個樹映射進去,見下文構造函數中memoryMap。

構造函數

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
        unpooled = false;
        this.arena = arena;
        this.memory = memory;
        this.pageSize = pageSize;
        this.pageShifts = pageShifts;
        this.maxOrder = maxOrder;
        this.chunkSize = chunkSize;
        this.offset = offset;
        unusable = (byte) (maxOrder + 1);
        log2ChunkSize = log2(chunkSize);
        subpageOverflowMask = ~(pageSize - 1);
        freeBytes = chunkSize;
        assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
        maxSubpageAllocs = 1 << maxOrder;
        memoryMap = new byte[maxSubpageAllocs << 1];
        depthMap = new byte[memoryMap.length];
        int memoryMapIndex = 1;
        for (int d = 0; d <= maxOrder; ++ d) {
            int depth = 1 << d;
            for (int p = 0; p < depth; ++ p) {
                memoryMap[memoryMapIndex] = (byte) d;
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
        }
        subpages = newSubpageArray(maxSubpageAllocs);
        cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
    }

關鍵參數

參數 說明
memory 申請的一塊內存大小爲16M,以字節數組表示
pageSize 8KB
pageShifts 13
maxOrder 11
chunkSize 16M
log2ChunkSize 14
subpageOverflowMask -8192
maxSubpageAllocs 2048
maxSubpageAllocs << 1 memoryMap數組的長度,4096
memoryMap 將二叉樹平鋪在該數組,格式見下文。
depthMap 同memoryMap將二叉樹平鋪在該數組

上面這些有些是直接傳入的,有的一眼看不出結果,代入公式算算。

log2ChunkSize 這個是chunkSize的對數,chunkSize大小是16M

@Test
public void testlog2ChunkSize(){

 System.out.println("16M的對數:" + log2(16384));

 System.out.println("2的14次方:" + Math.pow(214));
 
}

private static int log2(int val) {
 int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
 // compute the (0-based, with lsb = 0) position of highest set bit i.e, log2
 return INTEGER_SIZE_MINUS_ONE - Integer.numberOfLeadingZeros(val);
}

輸出:
16M的對數:14
214次方:16384.0

subpageOverflowMask 主要用於位運算的“&”操作判斷。

@Test
public void testSubpageOverflowMask(){
  int pageSize = 8192;
  int subpageOverflowMask = ~(pageSize - 1);
  System.out.println("subpageOverflowMask:" + subpageOverflowMask);
}

輸出:
subpageOverflowMask:-8192

maxSubpageAllocs 用於限制數組的長度

@Test
public void testMaxSubpageAllocs(){
   int maxOrder = 11;
   int maxSubpageAllocs = 1 << maxOrder;
   System.out.println("maxSubpageAllocs:" + maxSubpageAllocs);
   int length = maxSubpageAllocs << 1;
   System.out.println("memoryMap數組的長度:" + length);
}
輸出:
maxSubpageAllocs:2048
memoryMap數組的長度:4096

memoryMap&depthMap

這兩個數組主要把平衡二叉樹裝到了數組裏,外層循環控制層高,內層循環控制每層的節點數。直接看還是看不出到底啥格式,不要緊代入輸出看看。

 @Test
 public void testMemoryMap(){
  int maxOrder = 11;
  int memoryMapIndex = 1;
  int maxSubpageAllocs = 2048;
  byte[] memoryMap = new byte[maxSubpageAllocs << 1];
  byte[] depthMap = new byte[memoryMap.length];
  for (int d = 0; d <= maxOrder; ++ d) {
            int depth = 1 << d;
            for (int p = 0; p < depth; ++ p) {
                memoryMap[memoryMapIndex] = (byte) d;
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
   }
   System.out.println("memoryMap的長度:" + memoryMap.length);
   System.out.println("memoryMap數組內容:" + Arrays.toString(memoryMap));
}

輸出:
memoryMap的長度:4096
memoryMap數組內容:[001122223333333344444444444444445, ..... ]

小結

由輸出可以看出memoryMap的長度爲4096,內容格式如上,值表示所在的層,例如:數字1表示該節點在第樹的第1層;有兩個1表示該層有兩個葉子節點。也就是從2的0次方,一直到2的11次方,平鋪到了數組中。

平衡二叉樹查找更新過程

三次分配示例

Normal類型的內存分配,主要是如何在二叉樹中找到匹配的節點的過程,以及該節點的被分配後整個樹的狀態更新變化。

源碼部分在PoolChunk#allocateRun(int normCapacity) 部分,爲了方便debug和調試,把值代入後單獨拎出來跑一跑。

下面代碼可以直接運行,以執行三次分配,每次分配8KB的過程來看其對平衡二叉樹的查找過程。

public class PoolChunkTest {

    byte[] memoryMap = null;

    private final byte unusable = (byte) (11 + 1);

    public static void main(String [] args){
        PoolChunkTest poolChunkTest = new PoolChunkTest();
        poolChunkTest.initMemoryMap();
        int normCapacity = 8 * 1024;
        long id01 = poolChunkTest.allocateRun(normCapacity);
        System.out.println("第一次分配:" + id01);

        long id02 = poolChunkTest.allocateRun(normCapacity);
        System.out.println("第二次分配:" + id02);

        long id03 = poolChunkTest.allocateRun(normCapacity);
        System.out.println("第三次分配:" + id03);

    }

    private long allocateRun(int normCapacity) {
        int d = 11 - (log2(normCapacity) - 13);
        int id = allocateNode(d);

        return id;
    }

    private int allocateNode(int d) {
        int id = 1;
        int initial = - (1 << d); // has last d bits = 0 and rest all = 1
        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);
                System.out.println("val:" + val);
            }
        }
        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;
    }

    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;
        }
    }

    private void setValue(int id, byte val) {
        memoryMap[id] = val;
    }


    private static int log2(int val) {
        int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
        // compute the (0-based, with lsb = 0) position of highest set bit i.e, log2
        return INTEGER_SIZE_MINUS_ONE - Integer.numberOfLeadingZeros(val);
    }

    private byte value(int id) {
        return memoryMap[id];
    }


    public void initMemoryMap(){
        int maxOrder = 11;
        int memoryMapIndex = 1;
        int maxSubpageAllocs = 2048;
        memoryMap = new byte[maxSubpageAllocs << 1];
        byte[] depthMap = new byte[memoryMap.length];
        for (int d = 0; d <= maxOrder; ++ d) {
            int depth = 1 << d;
            for (int p = 0; p < depth; ++ p) {
                memoryMap[memoryMapIndex] = (byte) d;
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
        }
    }

}

輸出:

第一次分配:2048
第二次分配:2049
第三次分配:2050

例子中分配的8KB,根據公式 int d = 11 - (log2(normCapacity) - 13)算出其在11層,所以下文中三次分配時入參d=11。

第一分配8KB

第一次分配找到了數組memoryMap的下標爲2048,此時對應的值爲memoryMap[2048]=11。

當分配後將該節點標記爲不可用,也就是更新爲12(總共才11層,所以12爲不可用),此時memoryMap[2048]=12。

遞歸整棵樹,從下往上更新直到根節點,將父節點更新爲其子節點的最小值。例如:memoryMap[1024]的值原來爲10,被更新成了11.

第二次分配8KB

先找到了id=2048,這個節點發現其值爲12,也就是不可用了。此時memoryMap[2049]=11。

通過id ^= 1找到其兄弟節點id=2049,其對應的值爲11可用。返回該節點。

![image-20210314185941227](/Users/yongliang/Library/Application Support/typora-user-images/image-20210314185941227.png)

將節點id=2049設置爲不可用,即:memoryMap[2049]=12。

遞歸更新整棵樹,由於其父節點的兩個子節點都被分配出去了,所以1024被標記爲不可用。memoryMap[1024]=12。

第三次分配8KB

第三次分配8KB時,當循環到了節點1024,發現其不可用,也就是其子節點也不可用了。

通過id <<= 1找到1024的兄弟節點1025,接着向下查找。

找到節點1025的子節點2050發現其可用,即:memoryMap[2050]=11。找到後最後過程同上,標記其不可用表示已分配了,並更新整個樹把其父節點更新爲子節點的最小值。

平衡二叉樹查找更新圖示

第一次分配8KB前

整個樹都沒有被佔用,8KB會被選在第11層分配。

第一次分配8KB後

紅色標記爲變化部分,節點紅色表示被佔用。第11層的第一個節點memoryMap[2048]被標記爲不可用。第10層的第一個節點memoryMap[1024]值被從10更新爲11,整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。

第二次分配8KB後

第二次分配8KB後,第11層的第二個節點memoryMap[2049]被標記爲不可用,其父節點memoryMap[1024]由於其子節點都被分配完畢,也被標記爲不可用。整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。

第三次分配8KB後

第三次分配8KB後,將第11層的第三個節點memoryMap[2050]標記爲不可用,同時更新其父節點memoryMap[1025]爲子節點最小值11。整個樹會繼續向上遞歸變化,將整個樹父節點更新爲子節點最小的值。



本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章