切換語言為:簡體
Netty 申請記憶體入口 PoolArena 原始碼分析

Netty 申請記憶體入口 PoolArena 原始碼分析

  • 爱糖宝
  • 2024-07-02
  • 2064
  • 0
  • 0

PoolArena 是 Netty 申請記憶體的主要入口,Netty 借鑑 jemalloc 中 Arena 的設計思想,採用固定數量的多個 Arena 進行記憶體分配,預設數量通常為 CPU 核數 * 2。執行緒在首次申請分配記憶體時,會透過 round-robin 的方式輪詢 PoolArena 陣列,選擇一個固定的 PoolArena ,該執行緒在整個生命週期內都只會與該 PoolArena 打交道,所以每個執行緒都會儲存對應的 PoolArena 資訊,從而提高訪問效率。

本篇文章深入分析 PoolArena 的原始碼及核心原理。

Netty 申請記憶體入口 PoolArena 原始碼分析

PoolArena 的結構

PoolArena 是一個抽象類,它有兩個子類:DirectArena 和 HeapArena,其類圖如下:

Netty 申請記憶體入口 PoolArena 原始碼分析

PoolArena 繼承 SizeClass,實現 PoolArenaMetric 介面。PoolArenaMetric 提供了一些方法來獲取 PoolArena的指標資訊,它可以讓我們更好地瞭解記憶體池的使用情況,以便最佳化和調優應用程式。

PoolArena 重要的屬性如下:

abstract class PoolArena<T> extends SizeClasses implements PoolArenaMetric {
    enum SizeClass {
        Small,
        Normal
    }
  
    private final PoolSubpage<T>[] smallSubpagePools;

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;
    
    // ....    
}


從這裏可以看出,PoolArena 只有 Small 和 Norma 兩種記憶體規格,兩種記憶體規格,就有兩種記憶體分配的方式:

  1. PoolSubpage 型別的陣列:smallSubpagePools,用於分配小於 28K 的記憶體。

  2. 由 6 個 PoolChunkList 組成的雙向連結串列:用於分配小於 4MB 的記憶體。

結構如下:

Netty 申請記憶體入口 PoolArena 原始碼分析

PoolArena 的建構函式

建構函式如下:

    protected PoolArena(PooledByteBufAllocator parent, int pageSize,
          int pageShifts, int chunkSize, int cacheAlignment) {
        super(pageSize, pageShifts, chunkSize, cacheAlignment);
        
        // 所屬分配器
        this.parent = parent;
        directMemoryCacheAlignment = cacheAlignment;
         
        // 39
        numSmallSubpagePools = nSubpages; 
        smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
        for (int i = 0; i < smallSubpagePools.length; i ++) {
            // 初始化 Subpage 首節點
            smallSubpagePools[i] = newSubpagePoolHead();
        }

        q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
        q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
        q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
        q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
        q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
        qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

        q100.prevList(q075);
        q075.prevList(q050);
        q050.prevList(q025);
        q025.prevList(q000);
        q000.prevList(null);
        qInit.prevList(qInit);

        List<PoolChunkListMetric> metrics = new ArrayList<PoolChunkListMetric>(6);
        metrics.add(qInit);
        metrics.add(q000);
        metrics.add(q025);
        metrics.add(q050);
        metrics.add(q075);
        metrics.add(q100);
        chunkListMetrics = Collections.unmodifiableList(metrics);
    }


建構函式主要是初始化 smallSubpagePools 陣列和 PoolChunkList 雙向連結串列 。這裏重點講下 PoolChunkList 雙向連結串列。從建構函式中我們可以看到該雙向連結串列由 6 個節點組成,每個節點代表不同的記憶體使用率,如下:

  • qInit,記憶體使用率為 0% ~ 25% 的 Chunk。

  • q000,記憶體使用率為 1% ~ 50% 的 Chunk。

  • q025,記憶體使用率為 25% ~ 75% 的 Chunk。

  • q050,記憶體使用率為 50% ~ 100% 的 Chunk。

  • q075,記憶體使用率為 75% ~ 100% 的 Chunk。

  • q100,記憶體使用率為 100% 的 Chunk。

構建的雙向連結串列結構如下

Netty 申請記憶體入口 PoolArena 原始碼分析

針對這個結構,有兩個問題需要解答:

  1. qInit 和 q000 有什麼區別?這樣相似的兩個節點為什麼不設計成一個?

  2. 節點與節點之間的記憶體使用率重疊很大,為什麼要這麼設計?

第一個問題:qInit 和 q000 有什麼區別?這樣相似的兩個節點為什麼不設計成一個?

仔細觀察這個 PoolChunkList 的雙向連結串列,你會發現它並不是一個完全的雙向連結串列,它與完全的雙向連結串列有兩個區別:

  1. qInit 的 前驅節點是自己。這就意味著在 qInit 節點中的 PoolChunk 使用率到達 0% 後,它並不會被回收。

  2. q000 則沒有前驅節點,這樣就導致一個問題,隨著 PoolChunk 的記憶體使用率降低,直到小於 1% 後,它並不會退回到 qInit 節點,而是等待完全釋放後被回收。

所以如果某個 PoolChunk 的記憶體使用率一直都在 0 ~ 25% 之間波動,那麼它就可以一直停留在 qInit 中,這樣就避免了重複的初始化工作,故而 qInit 的作用主要在於避免某 PoolChunk 的記憶體使用變化率不大的情況下的頻繁初始化和釋放,提高記憶體分配的效率。而 q000 則用於 PoolChunk 記憶體使用變化率較大,待完全釋放後進行記憶體回收,防止永遠駐留在記憶體中。

qInit 和 q000 的配合使用,使得 Netty 的記憶體分配和回收效率更高效了。

第二個問題:節點與節點之間的記憶體使用率重疊很大,為什麼要這麼設計?

我們先看下圖:

Netty 申請記憶體入口 PoolArena 原始碼分析

從上圖可以看出,這些節點幾乎有一半空間是重疊的,為什麼要這麼設計呢?我們假定,q025 的範圍為 [25%,50%),q050 的範圍為 [50%,75%),如果有一個 PoolChunk 它的記憶體使用率變化情況為 40%、55%、45%、60%、48%,66%,這樣就會導致這個 PoolChunk 會在 q025 、q050 這兩個 PoolChunkList 不斷移動,勢必會造成效能損耗。如果範圍是 [25%,75%) 和 [50%,100%),這樣的記憶體使用率變化情況只會在 q025 中,只要當記憶體使用率超過了 75% 纔會移動到 q050,而隨著該 PoolChunk 的記憶體使用率降低,它也不是降到 75% 就回到 q025,而是要到 50%,這樣可以調整的範圍就大的多了。

記憶體分配

PoolArena 提供了 allocate() 用於記憶體分配,該方法根據申請記憶體的大小規格來分配不同規格的記憶體:

    PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
        PooledByteBuf<T> buf = newByteBuf(maxCapacity);
        allocate(cache, buf, reqCapacity);
        return buf;
    }
    
    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        // 根據 size 計算 sizeIdex
        final int sizeIdx = size2SizeIdx(reqCapacity);

        if (sizeIdx <= smallMaxSizeIdx) {
            // Small 規格,在 PoolSubpage 中分配
            tcacheAllocateSmall(cache, buf, reqCapacity, sizeIdx);
        } else if (sizeIdx < nSizes) {
            // Normal 規則,在 PoolChunk 中分配
            tcacheAllocateNormal(cache, buf, reqCapacity, sizeIdx);
        } else {
            // Huge 規格,直接分配
            int normCapacity = directMemoryCacheAlignment > 0
                    ? normalizeSize(reqCapacity) : reqCapacity;
            allocateHuge(buf, normCapacity);
        }
    }


首先根據申請的記憶體大小 reqCapacity 計算 sizeIdex,sizeIdex 是在 SizeClass 中計算的,如下:

   public int size2SizeIdx(int size) {
        if (size == 0) {
            return 0;
        }
        if (size > chunkSize) {
            return nSizes;
        }

        size = alignSizeIfNeeded(size, directMemoryCacheAlignment);
        
        // 對於小於 lookupMaxSize 這段,可以直接在 size2idxTab 表中取
        if (size <= lookupMaxSize) {
            return size2idxTab[size - 1 >> LOG2_QUANTUM];
        }
        
        // 這裏要跟計算 size 的個公式來倒推,大明哥數學都還給老師就不推到了
        int x = log2((size << 1) - 1);
        int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);

        int group = shift << LOG2_SIZE_CLASS_GROUP;

        int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;

        int deltaInverseMask = -1 << log2Delta;
        int mod = (size - 1 & deltaInverseMask) >> log2Delta &
                  (1 << LOG2_SIZE_CLASS_GROUP) - 1;

        return group + mod;
    }


得到 sizeIdex 後我們就可以確認使用哪種方式來進行記憶體分配:

  • Small:[0,38]

  • Normal:[39,68]

  • Huge:(68,)

tcacheAllocateSmall:Small 規格

tcacheAllocateSmall() 用於分配 Small 規格的記憶體:

    private void tcacheAllocateSmall(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity,
                                     final int sizeIdx) {
        // 使用快取
        if (cache.allocateSmall(this, buf, reqCapacity, sizeIdx)) {
            // was able to allocate out of the cache so move on
            return;
        }

        // 確定是哪個 PoolSubpage 塊
        final PoolSubpage<T> head = smallSubpagePools[sizeIdx];
        final boolean needsNormalAllocation;
        // 鎖定整個連結串列
        synchronized (head) {
            final PoolSubpage<T> s = head.next;
            needsNormalAllocation = s == head;
            // 這裏表示該連結串列中有空閒的記憶體可供分配
            if (!needsNormalAllocation) {
                assert s.doNotDestroy && s.elemSize == sizeIdx2size(sizeIdx) : "doNotDestroy=" +
                        s.doNotDestroy + ", elemSize=" + s.elemSize + ", sizeIdx=" + sizeIdx;
                long handle = s.allocate();
                assert handle >= 0;
                s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity, cache);
            }
        }
        
        // needsNormalAllocation == true,說明該 PoolSubpage 中沒有對應的記憶體,需要從 PoolChunk 中分配 PoolSubpage
        if (needsNormalAllocation) {
            synchronized (this) {
                allocateNormal(buf, reqCapacity, sizeIdx, cache);
            }
        }
        
        // allocationsSmall count + 1
        incSmallAllocation();
    }


  1. PoolThreadCache 快取中是否存在,有就直接分配即可

  2. 如果在 PoolThreadCache 快取中沒有,則從 smallSubpagePools 陣列中取,這裏需要注意,因為併發的關係,這裏使用了 synchronized (head) 來保證執行緒安全,鎖定 head 就是鎖定整個連結串列。

  3. 如果 head.next == head 說明當前連結串列中沒有空閒的記憶體可分配,需要從 PoolChunk 中分配 PoolSubpage。

tcacheAllocateNormal:Normal 規格

tcacheAllocateNormal() 用於分配 Normal 規格的記憶體。

    private void tcacheAllocateNormal(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity,
                                      final int sizeIdx) {
        if (cache.allocateNormal(this, buf, reqCapacity, sizeIdx)) {
            // was able to allocate out of the cache so move on
            return;
        }
        // 注意這裏是對整個 PoolArena 加鎖
        synchronized (this) {
            allocateNormal(buf, reqCapacity, sizeIdx, cache);
            ++allocationsNormal;
        }
    }


因為 Normal 規格的記憶體需要從 PoolChunk 中分配,其主要是利用 5種不同型別的 PoolChunkList 來進行分配,而一個 PoolArena 中只有一個 PoolChunkList 連結串列,所以需要對整個 PoolArena 加鎖。

    private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache threadCache) {
        if (q050.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q025.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q000.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            qInit.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q075.allocate(buf, reqCapacity, sizeIdx, threadCache)) {
            return;
        }

        // 生成一個新的 PoolChunk
        PoolChunk<T> c = newChunk(pageSize, nPSizes, pageShifts, chunkSize);
        boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
        assert success;
        // 加入到 qInit
        qInit.add(c);
    }


從這個方法我們可以看出,在 PoolChunkList 雙向連結串列中它並不是從 qInit 到 q100 按照順序來分配的,而是按照q050 —> q025 —> q000 —> qInit —> q075 這樣的順序,這樣做的目的是這樣的順序記憶體分配效率相對更高些。

allocateHuge:Huge 規格

allocateHuge() 用於分配 Huge 規格的記憶體,其分配方式是不進行池化處理,直接從堆或者堆外記憶體分配。

    private void allocateHuge(PooledByteBuf<T> buf, int reqCapacity) {
        PoolChunk<T> chunk = newUnpooledChunk(reqCapacity);
        activeBytesHuge.add(chunk.chunkSize());
        buf.initUnpooled(chunk, reqCapacity);
        allocationsHuge.increment();
    }

記憶體釋放

PoolArena 提供了 free() 用於對記憶體進行釋放:

    void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) { 
            // 非池化,直接釋放即可
            int size = chunk.chunkSize();
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            SizeClass sizeClass = sizeClass(handle);
            // 加入到快取中
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                // cached so not free it.
                return;
            }
            
            // 釋放記憶體
            freeChunk(chunk, handle, normCapacity, sizeClass, nioBuffer, false);
        }
    }


  • 對於 Huge 這類沒有池化的記憶體,則直接釋放 PoolChunk 即可。

  • 對於池化的記憶體,優先加入到 PoolThreadCache 快取中,如果新增失敗的話,則呼叫 freeChunk() 釋放記憶體

    void freeChunk(PoolChunk<T> chunk, long handle, int normCapacity, SizeClass sizeClass, ByteBuffer nioBuffer,boolean finalizer) {
        final boolean destroyChunk;
        // 加鎖
        synchronized (this) {
            // 在 PoolChunkList 中進行釋放,並調整其對應的數據結構
            destroyChunk = !chunk.parent.free(chunk, handle, normCapacity, nioBuffer);
        }
        if (destroyChunk) {
            destroyChunk(chunk);
        }
    }

由於 PoolArena 只是記憶體的分配和釋放的入口,真正執行記憶體分配的是在 PoolChunk 和 PoolSubpage 中,所以這篇文章在記憶體分配和釋放地方並沒深入到這兩個類當中,在後麵講解 PoolChunk 和 PoolSubpage 時再詳細深入分析。


轉自:大明哥_

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.