Netty原始碼分析(七) PoolChunk

weixin_34293059發表於2018-11-07

在分析原始碼之前,我們先來了解一下Netty的記憶體管理機制。我們知道,jvm是自動管理記憶體的,這帶來了一些好處,在分配記憶體的時候可以方便管理,也帶來了一些問題。jvm每次分配記憶體的時候,都是先要去堆上申請記憶體空間進行分配,這就帶來了很大的效能上的開銷。當然,也可以使用堆外記憶體,Netty就用了堆外記憶體,但是記憶體的申請和釋放,依然需要效能的開銷。所以Netty實現了記憶體池來管理記憶體的申請和使用,提高了記憶體使用的效率。
PoolChunk就是Netty的記憶體管理的一種實現。Netty一次向系統申請16M的連續記憶體空間,這塊記憶體通過PoolChunk物件包裝,為了更細粒度的管理它,進一步的把這16M記憶體分成了2048個頁(pageSize=8k)。頁作為Netty記憶體管理的最基本的單位 ,所有的記憶體分配首先必須申請一塊空閒頁。Ps: 這裡可能有一個疑問,如果申請1Byte的空間就分配一個頁是不是太浪費空間,在Netty中Page還會被細化用於專門處理小於4096Byte的空間申請 那麼這些Page需要通過某種資料結構跟演算法管理起來。
先來看看PoolChunk有哪些屬性

/**
 * 所屬 Arena 物件
 */
final PoolArena<T> arena;
/**
 * 記憶體空間。
 *
 * @see PooledByteBuf#memory
 */
final T memory;
/**
 * 是否非池化
 *
 * @see #PoolChunk(PoolArena, Object, int, int) 非池化。當申請的記憶體大小為 Huge 型別時,建立一整塊 Chunk ,並且不拆分成若干 Page
 * @see #PoolChunk(PoolArena, Object, int, int, int, int, int) 池化
 */
final boolean unpooled;

final int offset;

/**
 * 分配資訊滿二叉樹
 *
 * index 為節點編號
 */
private final byte[] memoryMap;
/**
 * 高度資訊滿二叉樹
 *
 * index 為節點編號
 */
private final byte[] depthMap;
/**
 * PoolSubpage 陣列
 */
private final PoolSubpage<T>[] subpages;
/**
 * 判斷分配請求記憶體是否為 Tiny/Small ,即分配 Subpage 記憶體塊。
 *
 * Used to determine if the requested capacity is equal to or greater than pageSize.
 */
private final int subpageOverflowMask;
/**
 * Page 大小,預設 8KB = 8192B
 */
private final int pageSize;
/**
 * 從 1 開始左移到 {@link #pageSize} 的位數。預設 13 ,1 << 13 = 8192 。
 *
 * 具體用途,見 {@link #allocateRun(int)} 方法,計算指定容量所在滿二叉樹的層級。
 */
private final int pageShifts;
/**
 * 滿二叉樹的高度。預設為 11 。
 */
private final int maxOrder;
/**
 * Chunk 記憶體塊佔用大小。預設為 16M = 16 * 1024  。
 */
private final int chunkSize;
/**
 * log2 {@link #chunkSize} 的結果。預設為 log2( 16M ) = 24 。
 */
private final int log2ChunkSize;
/**
 * 可分配 {@link #subpages} 的數量,即陣列大小。預設為 1 << maxOrder = 1 << 11 = 2048 。
 */
private final int maxSubpageAllocs;

Netty採用完全二叉樹進行管理,樹中每個葉子節點表示一個Page,即樹高為12,中間節點表示頁節點的持有者。有了上面的資料結構,那麼頁的申請跟釋放就非常簡單了,只需要從根節點一路遍歷找到可用的節點即可。主要來看看PoolChunk是怎麼分配記憶體的。

long allocate(int normCapacity) {
    // 大於等於 Page 大小,分配 Page 記憶體塊
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        return allocateRun(normCapacity);
    // 小於 Page 大小,分配 Subpage 記憶體塊
    } else {
        return allocateSubpage(normCapacity);
    }
}

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

private long allocateSubpage(int normCapacity) {
    // 獲得對應記憶體規格的 Subpage 雙向連結串列的 head 節點
    // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
    // This is need as we may add it back and so alter the linked-list structure.
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    // 加鎖,分配過程會修改雙向連結串列的結構,會存在多執行緒的情況。
    synchronized (head) {
        // 獲得最底層的一個節點。Subpage 只能使用二叉樹的最底層的節點。
        int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
        int id = allocateNode(d);
        // 獲取失敗,直接返回
        if (id < 0) {
            return id;
        }

        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;

        // 減少剩餘可用位元組數
        freeBytes -= pageSize;

        // 獲得節點對應的 subpages 陣列的編號
        int subpageIdx = subpageIdx(id);
        // 獲得節點對應的 subpages 陣列的 PoolSubpage 物件
        PoolSubpage<T> subpage = subpages[subpageIdx];
        // 初始化 PoolSubpage 物件
        if (subpage == null) { // 不存在,則進行建立 PoolSubpage 物件
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else { // 存在,則重新初始化 PoolSubpage 物件
            subpage.init(head, normCapacity);
        }
        // 分配 PoolSubpage 記憶體塊
        return subpage.allocate();
    }
}

Netty的記憶體按大小分為tiny,small,normal,而型別上可以分為PoolChunk,PoolSubpage,小於4096大小的記憶體就被分成PoolSubpage。Netty就是這樣實現了對記憶體的管理。
PoolChunk就分析到這裡了。

相關文章