Netty16# 池化內存Subpage類型內存分配

前言

前面聊了大於8KB的內存分配,那小於8KB的呢?上一篇的平衡二叉樹第十一層的葉子節點最小也是8KB,那比如要分配128B的緩存,直接分給8KB顯然是不合適的,Tiny是小於512Byte,Small介於512B~8KB,Tiny和Small統稱Subpage,本文就聊聊他們的內存分配情況,這塊應該是整個netty最爲複雜的部分了。

內容提要

下面是以分配128B爲例的整體流程架構圖,下面大體敘述下其流程。

  • 先從平衡二叉樹的第11層選一個未分配的葉子節點大小爲8KB的一個Page
    備註:本例中爲memoryMap[2048]

  • 對該Page進行切割,假如要分配128B,整體會切割爲64塊
    備註:8192/128=64

  • 通過long類型二進制64位來標記分割成各個塊的分配狀態

    備註:0:未分配,1:已分配

  • 一個bitmap數組長度爲8,每個元素都能對64塊內存進行標記

  • 建立了二叉樹節點與切分塊之間的映射關係
    備註:memoryMapIdx ^ maxSubpageAllocs

  • 分配後建立二叉樹葉子節點與標記位之間的關係,可以指向內存一塊區域
    備註:0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx

源碼分析

示例代碼

@Test
public void testAllocateSubpage() {
    ByteBufAllocator allocator = new PooledByteBufAllocator();
    allocator.directBuffer(128); 
}

備註:以分配128B的內存爲例,分析其分配過程。

源碼分析

private long allocateSubpage(int normCapacity) {
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity); // 註解@1
   int d = maxOrder; 
    synchronized (head) {
        int id = allocateNode(d); // 註解@2
     if (id < 0) {
            return id;
        }
        final PoolSubpage<T>[] subpages = this.subpages; // 註解@3
        final int pageSize = this.pageSize;
        freeBytes -= pageSize;
        int subpageIdx = subpageIdx(id); // 註解@4

        PoolSubpage<T> subpage = subpages[subpageIdx]; 
        if (subpage == null) { // 註解@5
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            subpage.init(head, normCapacity);
        }
        return subpage.allocate(); // 註解@6
    }
}

註解@1 從tinySubpagePools中獲取PoolSubpage。獲取過程爲elemSize >>> 4(除以16)來獲取。

tinySubpagePools結構

tinySubpagePools被初始化成長度爲32的數組,元素之間差額爲16B。

註解@2 allocateNode 在上一篇文章分析過,d = maxOrder = 1。表示在平衡二叉樹的第11層找到可分配的節點,具體爲memoryMap數組中的下標。如果整個樹都沒有內存可分配了,返回的id=-1。

註解@3 先看下subpages的初始化,maxSubpageAllocs = 1 << maxOrder= 2048。也就是PoolSubpage [] subpages的長度爲平衡二叉樹第11層所有的節點數(2^11)。

subpages = newSubpageArray(maxSubpageAllocs);

註解@4 將平衡二叉樹第11層的下標memoryMap[]的下標轉換爲subpages[]數組的下標。轉換關係爲memoryMapIdx ^ maxSubpageAllocs。

例如:平衡二叉樹第11層第1個節點數組下標爲2048,轉換爲subpages的下標爲0,平衡二叉樹第11層第2個節點數組下標爲2049,轉換爲subpages的下標爲1,平衡二叉樹第11層第2個節點數組下標爲2050,轉換爲subpages的下標爲2。

註解@5 初始化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]; // pageSize / 16 / 64
  init(head, elemSize);
}

參數說明

head: PoolSubpage數組中的一個元素,本例中爲第4個元素

chunk: 當前PoolChunk實例

memoryMapIdx: 平衡二叉樹第11層用於分配的節點,具體爲memoryMap數組下標

elemSize: 待分配的內存,本例中爲128KB

bitmap: long數組長度爲8「8192無符號右移10位=8」

初始化說明

void init(PoolSubpage<T> head, int elemSize) {
     doNotDestroy = true;
    // 待分配內存
     this.elemSize = elemSize; 
     if (elemSize != 0) {
            // maxNumElems表示可以被切割成幾份(8192除以待分配內存)例如:64=8192/128被切成了64份
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            // 無符號右移6位,高位補零(相當於除以64)例如:64的二進制右移6位爲1,128的二進制右移6位爲2
            bitmapLength = maxNumElems >>> 6;
            if ((maxNumElems & 63) != 0) { // 相當於是否能被64整除
                bitmapLength ++; // 不能被整除遞增bitmapLength
            }

            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0// 等於零表示未被分配
            }
     }
     addToPool(head);
}

過程說明

@1 先計算一個Page被切成了幾份 maxNumElems( pageSize / elemSize)

@2 計算bitmap數組長度bitmapLength(maxNumElems無符號右移6位相當於除以64)

備註: 此處不太好理解爲什麼要maxNumElems要除以64來計算bitmap的長度呢?也就是bitmap數組中的每個元素可以標記64個被切的內存塊。bitmap是long數組,每個long類型是64位,他用每個二進制位來標記被切內存塊的分配情況。

加入鏈表

新構建的PoolSubpage與tinySubpagePools中的PoolSubpage建成鏈表關係。

private void addToPool(PoolSubpage<T> head) {
    assert prev == null && next == null;
    prev = head;
    next = head.next;
    next.prev = this;
    head.next = this;
}

小結: 構造的PoolSubpage中持有了一個bitmap[]數組,數組長度與待分配的內存有關。待分配內存大小爲elemSize,數組長度=PageSize/elemSize,並將bitmap數組的元素標記爲未分配。

註解@6 分配內存

內存的分配以兩次分配128B內存爲例觀察期分配過程。

@Test
public void testAllocateSubpage() {
   ByteBufAllocator allocator = new PooledByteBufAllocator();
   allocator.directBuffer(128); // 第一次分配
   allocator.directBuffer(128); // 第二次分配
}

第一次分配

第二次分配

第一次輪詢第一位已被佔用,需要向右移位。

第二次輪詢第二位未被佔用。

第二次分配過程

兩次內存分配圖示

第一次分配128B圖示

此時64位第一位被標記爲1,bitmap[0] = 1

第二次分配128B圖示

此時64位第二位也被標記爲1,bitmap[0] = 3



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

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