歡迎光臨
每天分享高質量文章

【Netty 專欄】深入淺出 Netty 記憶體管理 PoolArena

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 


摘要: 原創出處 https://www.jianshu.com/p/4856bd30dd56 「佔小狼」歡迎轉載,保留摘要,謝謝!

  • PoolArena


前面分別分析了PoolChunk、PoolSubpage和PoolChunkList,本文主要分析PoolArena。
1、深入淺出Netty記憶體管理 PoolChunk
2、深入淺出Netty記憶體管理 PoolSubpage
3、深入淺出Netty記憶體管理 PoolChunkList

PoolArena

應用層的記憶體分配主要透過如下實現,但最終還是委託給PoolArena實現。

PooledByteBufAllocator.DEFAULT.directBuffer(128);

由於netty通常應用於高併發系統,不可避免的有多執行緒進行同時記憶體分配,可能會極大的影響記憶體分配的效率,為了緩解執行緒競爭,可以透過建立多個poolArena細化鎖的粒度,提高併發執行的效率。

先看看poolArena的內部結構:

img

poolArena

所有記憶體分配的size都會經過normalizeCapacity進行處理,當size>=512時,size成倍增長512->1024->2048->4096->8192,而size<512則是從16開始,每次加16位元組。

poolArena提供了兩種方式進行記憶體分配:

  1. PoolSubpage用於分配小於8k的記憶體;

  • tinySubpagePools:用於分配小於512位元組的記憶體,預設長度為32,因為記憶體分配最小為16,每次增加16,直到512,區間[16,512)一共有32個不同值;

  • smallSubpagePools:用於分配大於等於512位元組的記憶體,預設長度為4;

  • tinySubpagePools和smallSubpagePools中的元素都是預設subpage。

  1. poolChunkList用於分配大於8k的記憶體;

  • qInit:儲存記憶體利用率0-25%的chunk

  • q000:儲存記憶體利用率1-50%的chunk

  • q025:儲存記憶體利用率25-75%的chunk

  • q050:儲存記憶體利用率50-100%的chunk

  • q075:儲存記憶體利用率75-100%的chunk

  • q100:儲存記憶體利用率100%的chunk

img

poolChunkList

  1. qInit前置節點為自己,且minUsage=Integer.MIN_VALUE,意味著一個初分配的chunk,在最開始的記憶體分配過程中(記憶體使用率<25%),即使完全釋放也不會被回收,會始終保留在記憶體中。

  2. q000沒有前置節點,當一個chunk進入到q000串列,如果其記憶體被完全釋放,則不再保留在記憶體中,其分配的記憶體被完全回收。

接下去看看poolArena如何實現記憶體的分配,實現如下:

private void allocate(PoolThreadCache cache, PooledByteBuf buf, final int reqCapacity) {
    final int normCapacity = normalizeCapacity(reqCapacity);
    if (isTinyOrSmall(normCapacity)) { // capacity 
        int tableIdx;
        PoolSubpage[] table;
        boolean tiny = isTiny(normCapacity);
        if (tiny) { // 
            if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            tableIdx = tinyIdx(normCapacity);
            table = tinySubpagePools;
        } else {
            if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            tableIdx = smallIdx(normCapacity);
            table = smallSubpagePools;
        }

        final PoolSubpage head = table[tableIdx];

        /**
         * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
         * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
         */

        synchronized (head) {
            final PoolSubpage s = head.next;
            if (s != head) {
                assert s.doNotDestroy && s.elemSize == normCapacity;
                long handle = s.allocate();
                assert handle >= 0;
                s.chunk.initBufWithSubpage(buf, handle, reqCapacity);

                if (tiny) {
                    allocationsTiny.increment();
                } else {
                    allocationsSmall.increment();
                }
                return;
            }
        }
        allocateNormal(buf, reqCapacity, normCapacity);
        return;
    }
    if (normCapacity <= chunkSize) {
        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
            // was able to allocate out of the cache so move on
            return;
        }
        allocateNormal(buf, reqCapacity, normCapacity);
    } else {
        // Huge allocations are never served via the cache so just call allocateHuge
        allocateHuge(buf, reqCapacity);
    }
}

1、預設先嘗試從poolThreadCache中分配記憶體,PoolThreadCache利用ThreadLocal的特性,消除了多執行緒競爭,提高記憶體分配效率;首次分配時,poolThreadCache中並沒有可用記憶體進行分配,當上一次分配的記憶體使用完並釋放時,會將其加入到poolThreadCache中,提供該執行緒下次申請時使用。
2、如果是分配小記憶體,則嘗試從tinySubpagePools或smallSubpagePools中分配記憶體,如果沒有合適subpage,則採用方法allocateNormal分配記憶體。
3、如果分配一個page以上的記憶體,直接採用方法allocateNormal分配記憶體。

allocateNormal實現如下:

private synchronized void allocateNormal(PooledByteBuf buf, int reqCapacity, int normCapacity) {
    ++allocationsNormal;
    if (q050.allocate(buf, reqCapacity, normCapacity)
     || q025.allocate(buf, reqCapacity, normCapacity)
     || q000.allocate(buf, reqCapacity, normCapacity)
     || qInit.allocate(buf, reqCapacity, normCapacity)
     || q075.allocate(buf, reqCapacity, normCapacity)
     || q100.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }

    // Add a new chunk.
    PoolChunk c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    long handle = c.allocate(normCapacity);
    assert handle > 0;
    c.initBuf(buf, handle, reqCapacity);
    qInit.add(c);
}

第一次進行記憶體分配時,chunkList沒有chunk可以分配記憶體,需透過方法newChunk新建一個chunk進行記憶體分配,並新增到qInit串列中。如果分配如512位元組的小記憶體,除了建立chunk,還有建立subpage,PoolSubpage在初始化之後,會新增到smallSubpagePools中,其實並不是直接插入到陣列,而是新增到head的next節點。下次再有分配512位元組的需求時,直接從smallSubpagePools獲取對應的subpage進行分配。

img

smallSubpagePools

分配記憶體時,為什麼不從記憶體使用率較低的q000開始?在chunkList中,我們知道一個chunk隨著記憶體的釋放,會往當前chunklist的前一個節點移動。

q000存在的目的是什麼?
q000是用來儲存記憶體利用率在1%-50%的chunk,那麼這裡為什麼不包括0%的chunk?
直接弄清楚這些,才好理解為什麼不從q000開始分配。q000中的chunk,當記憶體利用率為0時,就從連結串列中刪除,直接釋放物理記憶體,避免越來越多的chunk導致記憶體被佔滿。

想象一個場景,當應用在實際執行過程中,碰到訪問高峰,這時需要分配的記憶體是平時的好幾倍,當然也需要建立好幾倍的chunk,如果先從q0000開始,這些在高峰期建立的chunk被回收的機率會大大降低,延緩了記憶體的回收進度,造成記憶體使用的浪費。

那麼為什麼選擇從q050開始?
1、q050儲存的是記憶體利用率50%~100%的chunk,這應該是個折中的選擇!這樣大部分情況下,chunk的利用率都會保持在一個較高水平,提高整個應用的記憶體利用率;
2、qinit的chunk利用率低,但不會被回收;
3、q075和q100由於記憶體利用率太高,導致記憶體分配的成功率大大降低,因此放到最後;




如果你對 Dubbo 感興趣,歡迎加入我的知識星球一起交流。

知識星球

目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 伺服器

16. P2P 伺服器

17. HTTP 伺服器

18. 序列化 Serialization

19. 叢集容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing


一共 60 篇++

原始碼不易↓↓↓

點贊支援老艿艿↓↓

贊(0)

分享創造快樂