新四季網

netty 直接內存(超長圖文詳解Netty的高性能內存管理)

2023-09-19 21:33:05 2

Netty 可謂是當下最流行的網絡編程框架,它被廣泛應用在中間件、直播、社交、遊戲等領域。目前,許多知名的開源軟體也都將 Netty 用作網絡通信的底層框架,如 Dubbo、RocketMQ、Elasticsearch、HBase 等。 Netty 作為一款高性能的網絡框架,除了優秀的線程模型,高性能的內存管理也是必不可少的。

前言

Netty 中的內存管理,如果從廣義來說,ByteBuf 內存容器、ReferenceCounted 和 ResourceLeakDetector 等相關的內存洩露檢測、負責分配內存的 ByteBufAllocator、內存分配和管理算法、零拷貝等內容均屬於其範疇。下文所述的內存管理是從狹義上的 ByteBufAllocator 及 內存分配和管理算法出發,希望能給大家帶來幫助。

Netty 內存管理比較複雜,理解上可能有些晦澀,屏蔽掉相對不太重要的一些細節,從大框架上力爭講述清晰。下面講述自己的一些理解,同時也歡迎溝通,共同進步。

一、背景1.1 Netty 為什麼要實現內存管理?

Netty 作為底層網絡框架,為了更高效的網絡傳輸性能,堆外內存(Direct ByteBuffer)的使用是非常高頻的。堆外內存在 JVM 之外,在有效降低 JVM GC 壓力的同時,還能提高傳輸性能。但它也是一把雙刃劍,堆外內存是非常寶貴的資源,申請和釋放都是高成本的操作,使用不當還可能造成嚴重的內存洩露等問題 。那麼進行池化管理,多次重用是比較有效的方式。從申請內存大小的角度講,申請多大的 Direct ByteBuffer 進行池化又會是一大問題,太大會浪費內存,太小又會出現頻繁的擴容和內存複製!!!所以呢,就需要有一個合適的內存管理算法,解決高效分配內存的同時又解決內存碎片化的問題。所以一個優秀的內存管理算法必不可少。

一個內存分配器至少需要看關注兩個核心目標:

高效的內存分配和回收,提升單線程或者多線程場景下的性能提高內存的有效利用率,減少內存碎片,包括內部碎片和外部碎片1.2 Netty 為什麼選擇 Jemalloc 算法實現內存管理?

Jemalloc memory allocator:http://jemalloc.net/

Netty 官方引用的兩篇文檔:A Scalable Concurrent malloc(3) Implementation for FreeBSD(https://www.bsdcan.org/2006/papers/jemalloc.pdf) 和

Scalable memory allocation using jemalloc(https://www.facebook.com/notes/10158791475077200)

常見的內存分配器比較: ptmalloc、tcmalloc和jemalloc(http://www.cnhalo.net/2016/06/13/memory-optimize)

1.3 Netty 內存管理的改動優化?

近期公司內大佬近期的一篇文章讓我了解到 Netty 在2020年9月份的 4.1.52 版本中就做了一次較大的優化升級:https://github.com/netty/netty/pull/10267。這次的改動也讓 netty 的內存分配算法更接近原生的 jemalloc。核心改動如下:

去掉了 tiny 內存規格,僅保留:small、normal 和 huge,並重新劃分了內存規格。與之帶來夥伴分配算法的一些改動由於內存規格的改動,帶來 PoolChunk 內部結構的改動和 allocate 分配算法的改動PoolSubPage 原來要負責 tiny 和 small 內存規格的分配,自此只需服務於 small 內存規格。分配 Subpage 時,使用 pageSize 和 elemSize 的最小公共倍數而不再用 pageSize。

下文中我對於 Netty 內存管理的源碼分析是2019年3月份的 4.1.34 版本。從 4.1.34 版本至當前(2021年10月)的 4.1.68 版本之間,除去 4.1.52 版本的較大改動,其餘版本基本無算法層面的改動。以後向大佬們學習,能持續跟進最新發展趨勢。

4.1.52 版本雖然有不小的改動,不過對於內存管理的核心思路和邏輯變化並不大,替換了引擎中的部分零件。對於有興趣繼續閱讀下去這篇又臭又長又幹的文章小夥伴,也帶著辯證的角度去閱讀,讀完之後再回頭品一品這次的改動。

接下來就看看 Netty 在 4.1.52 版本前是怎麼實現 java 版的 jemalloc 的,以及向優秀的內存分配器核心目標做出細到極致的優化。

二、內存規格劃分和夥伴算法2.1 內存規格劃分

為了分配的內存塊儘可能保持連續、為了內存塊能盡大程度的被利用、為了減少內部碎片,Netty 對內存規格進行了細緻的劃分。

上圖第一列"分類"表示 Netty 對內存大小劃分為:Tiny、Small、Normal 和 Huge 四類。

Netty 默認向作業系統申請的內存大小為 16MB,對於大於 16MB 的內存定義為 Huge 類型,認為是:大型內存不做緩存、不做池化,直接以 Unpool 的形式分配內存,用完後回收。

對於 16MB 及更小的內存,分類為:Tiny、Small、Normal,也有對應的枚舉 SizeClass 進行描述。不過 Netty 定義了一套更細粒度的內存分配單位:Chunk、Page、Subpage,方便內存的管理。

Chunk 即上述提及的 Netty 向作業系統申請內存的單位,默認是 16MB。後續所有的內存分配也都是基於 Chunk 完成。Chunk 是 Page 的集合。

Page 是 Chunk 用於管理內存的基本單位。Page 的默認大小為 8KB,若欲申請 16KB,則需申請連續的兩塊空閒 Page。一個 Chunk(16MB),由 2048 個 Page (8KB)組成。

SubPage 是 Page 下的管理單位。對於底層應用,KB 級的內存已屬於大內存的範疇,更多的是 B 級的小內存,直接使用Page 進行內存的分配,無疑是非常浪費的。所以對 Page 進行了切割劃分,劃分後的便是 SubPage,Tiny 和 Small 類型的內存使用的分配單位都是 SubPage。切割劃分的算法原則是:如首次申請 512 B 的內存,則先申請一塊 Page 內存,然後將 8 KB 的 Page 按照 512B 均分為 16 塊,每一塊可以認為是一個 SubPage,然後將第一塊 SubPage 內存地址返回給申請方。同時下一次申請 512B 內存,則在 16 塊中分配第二塊。其他非 512B 的內存申請,則另外申請一個 Page 進行均等切分和分配。所以,對於 SubPage 沒有固定的大小,和 Tiny、Small 中某個具體大小的內存申請有關。

PS:為什麼只有上面窮舉出來的內存大小,沒有19B、21B、3KB這樣規格?是因為 netty 中會把申請內存大小通過io.netty.buffer.PoolArena#normalizeCapacity方法進行了標準化,向上取整到最接近的上圖中所列舉出的大小,以便於管理。

2.2 夥伴分配算法

Chunk作為向作業系統申請內存的單位,Page 作為 Chunk 管理內存的基本單位。Chunk 是通過夥伴算法 (Buddy system) 管理 Page,每個 Chunk 劃分成 2048 個 Page,最終通過一顆 depth = 12 的滿二叉樹(共4095個節點,僅2048個葉子作為 Page)實現。如下圖所示:

高度為 11 的節點(2048 - 4095)即為 Page 節點,代表 8 KB ;

高度為 10 的節點(1024 - 2047)均擁有 2 個 Page 節點,代表16 KB;

高度為 1 的節點(2、3)均擁有 1024 個 Page 節點,代表 8 MB;

高度為 0 的節點(1)擁有 2048 個 Page 節點,代表 16 MB,即一個滿 Chunk 的大小。

在 PoolChunk 中有兩個 byte 數組負責對 Page 分配。

memoryMap[] 和 depthMap[] 初識化完成時,如上圖所示,數組 index 代表樹的節點編號(從1開始,1-4095),數組 value 存出當前節點編號在樹中的高度(從0開始,0-11)。兩個數組的內容完全相同。depthMap[] 初始化完成後,便永遠不會變化,僅用來通過節點編號快速獲取樹的高度。depthMap[1024]=10、depthMap[2048]=11,畢竟數組查詢 O(1) 的時間複雜度,不需要每次在進行計算memoryMap[] 初識化完成後,根據節點的分配情況,value 值會進行相應的更改。以及根據 value 值判斷該節點是否可以被分配。

memoryMap[] 中的 value 值從小到大,會有下述三種狀態:

memoryMap[id] = depthMap[id] ,該節點沒有被分配。如初始化完成時此種狀態。depthMap[id] < memoryMap[id] < 最大高度(12)。至少有一個子節點被分配,但尚未完全被分配,不能再分配該高度對應的內存,只能根據實際分配較小一些的內存。memoryMap[id] = 最大高度(12) ,該節點及其子節點已被完全分配,沒有剩餘空間。

下面演示分配 Page 內存時,memoryMap[] 中 value 值的變化。

1、 memoryMap[] 還是一顆純潔的樹,內存還保持完整。分配 8 KB 內存時,變化如下。memoryMap[2048] 變為 12,直接進入狀態3(完全被分配); 遞歸遍歷2048的父節點,將父節點的值置為左右孩子節點中較小的值,如:memoryMap[1024]=11、memoryMap[1] = 0,這些節點都為狀態2(部分被分配)。 進入下圖狀態:

2、在上述基礎上,又有人申請分配 16 KB內存。原本樹高為 10 的這層節點代表 16KB 內存大小,由於memoryMap[1024]=11,1024號節點被分出去的 8KB,只剩 8KB 空間可用,無法滿足 16KB 的申請,所以在第10層順序向後尋找可用節點。

1025號節點,memoryMap[1025]=10(狀態1),符合條件,故將 1025 號節點分配。分配後 memoryMap[1025] 置為 12,進入狀態3,他所有的子節點也要置為12。同時遞歸遍歷 1025 號的父節點,將父節點的值置為左右孩子節點中較小的值。進入下圖狀態:

3、在上述基礎上,又有人來申請分配 8 KB內存。樹高為 11 的這層節點 2048 號節點找起,發現 memoryMap[2048]=12 不可用,在該層繼續向後遍歷找到 2049 號節點,memoryMap[2049]=11(狀態1),便將2049號節點分配出去,memoryMap[2049] 置為 12(狀態三)。同時遞歸遍歷 2049 號的父節點,將父節點的值置為左右孩子節點中較小的值。進入下圖狀態:

4、在上述基礎上,假如又有人來申請分配 8 MB 的內存。8MB 的內存節點在樹高為 1 的這層節點上。2 號節點 memoryMap[2]=2(狀態2),已不滿足 8 MB 的需求,在該層繼續向後遍歷找到 3 號節點,memoryMap[3]=1(狀態1),便將3號節點分配出去。此時 memoryMap[] 數組的狀態圖大家可自行想像一下。

上述分配算法位於 Netty 中的 io.netty.buffer.PoolChunk#allocateRun方法,有興趣可以看此方法的源碼,下文中也會介紹到。

三、ByteBufAllocator

介紹完了內存規格的劃分和分配,下面從前言提到的 ByteBufAllocator 做為真正內存管理算法的入口講起。

ByteBufAllocator 是用來分配和創建 ByteBuf 的,它是最頂層的接口,下圖是對它及各子類的梳理和總結,本文的關注點主要集中在 PooledByteBufAllocator 這個對 jemalloc 算法進行了 Java 版實現的池化內存分配器

下面列舉了下 PooledByteBufAllocator 中比較重要的一些變量,大概有個印象,這些變量貫穿整個內存分配過程。

// Heap 類型的 Arena 數量,默認(最小值):2*CPU核數private static final int DEFAULT_NUM_HEAP_ARENA; // direct 類型的 Arena 數量,默認(最小值):2*CPU核數private static final int DEFAULT_NUM_DIRECT_ARENA; // 默認 Page 的內存大小:8192B=8KBprivate static final int DEFAULT_PAGE_SIZE; // 滿二叉樹的高度,默認為 11 。8192 << 11 = 16 MiB per chunk。private static final int DEFAULT_MAX_ORDER; // PoolThreadCache 的 tiny 類型的內存塊的緩存數量。默認為 512private static final int DEFAULT_TINY_CACHE_SIZE; // PoolThreadCache 的 small 類型的內存塊的緩存數量。默認為 256private static final int DEFAULT_SMALL_CACHE_SIZE; // PoolThreadCache 的 normal 類型的內存塊的緩存數量。默認為 64private static final int DEFAULT_NORMAL_CACHE_SIZE; // PoolThreadCache 緩存的最大內存塊的字節數,默認:32*1024private static final int DEFAULT_MAX_CACHED_BUFFER_CAPACITY; // 是否使用PoolThreadCache。默認:true private static final boolean DEFAULT_USE_CACHE_FOR_ALL_THREADS; private final PoolArena[] heapArenas; // 默認值:DEFAULT_NUM_HEAP_ARENAprivate final PoolArena[] directArenas; // 默認值:DEFAULT_NUM_DIRECT_ARENAprivate final int tinyCacheSize; // 默認值:DEFAULT_TINY_CACHE_SIZEprivate final int smallCacheSize; // 默認值:DEFAULT_SMALL_CACHE_SIZEprivate final int normalCacheSize; // 默認值:DEFAULT_NORMAL_CACHE_SIZE// ThreadLocal線程變量,用於獲得 PoolThreadCache 對象。(大名鼎鼎的 FastThreadLocal)private final PoolThreadLocalCache threadCache; private final int chunkSize; // Chunk 大小,16MB

這裡簡單提下 heapArenas 和 directArenas。PooledByteBufAllocator 是既可以分配 JVM 的 Heap 堆內存,也可以分配堆外 Direct 內存,所以分配器中既含有用於 Heap 內存分配的 heapArenas,又有 Direct 內存分配的 directArenas。兩者分配過程的算法是基本一致的,不同的僅在於最底層向系統或 JVM 申請內存的方式。後面無特殊說明,默認按 Direct 內存的分配。

下面是我簡單梳理出的一張「流程圖」,它並非一張嚴謹的圖,僅僅表達整體的「控制」關係,便於大家對 Netty 中內存分配都由哪些類來完成有一個初步的認知。Page加了虛線框,是因為它只是個邏輯概念,並沒有實體類來承載,但又不避能開它,類似 Kafka 中的 topic 概念。

在 Netty 中如果需要進行池化的內存分配,代碼可以這麼寫:

// 傾向於 directBuffer 分配的池化分配器。【傾向】= 大多數情況是 directBuffer。部分場景是 heapBufferPooledByteBufAllocator pooledAllocator = PooledByteBufAllocator.DEFAULT;// 默認分配器:PooledByteBufAllocator.DEFAULT。// 若識別到當前 System 是安卓系統(相對伺服器,內存資源較為寶貴),則默認:UnpooledByteBufAllocator.DEFAULTPooledByteBufAllocator pooledAllocator2 = (PooledByteBufAllocator) ByteBufAllocator.DEFAULT;// 申請 32KB 的 directBuffer。 默認:directBuffer 分配器。ByteBuf byteBuf = pooledAllocator.buffer(1024 * 32);// 申請 16KB 的 directBufferByteBuf byteBuf2 = pooledAllocator2.directBuffer(1024 * 16);// 申請 16KB 的 heapBufferByteBuf byteBuf3 = pooledAllocator2.heapBuffer(1024 * 32);

上面 directBuffer 方法經過簡單的校驗,最終會走到下面 newDirectBuffer 這個方法,整體邏輯也很簡單。

protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { // 從 ThreadLocal 中獲得 PoolThreadCache 對象,並獲得對應的 directArena 對象 PoolThreadCache cache = threadCache.get; PoolArena directArena = cache.directArena; // ... // 從 directArena 中分配內存,請見下文分解 buf = directArena.allocate(cache, initialCapacity, maxCapacity); // ... // 將 ByteBuf 裝飾成 LeakAware ( 可檢測內存洩露 )的 ByteBuf 對象 return toLeakAwareBuffer(buf);}

這裡有個優秀的設計思想值得我們學習。PoolArena[] directArenas 這個數組的大小是 2 * CPU 核數,在 ThreadLocal 初始化時會從 directArenas 數組中選取一個被線程引用最少的 PoolArena,這樣做的目的是為了分散並發度,降低單個 PoolArena 的並發,緩解資源競爭的問題,以提高內存分配效率。

上面代碼的內存分配邏輯最終落到了io.netty.buffer.PoolArena#allocate,接下來我們就看看 PoolArena 中做了什麼事情。

四、PoolArena4.1 初識 PoolArena

在 io.netty.buffer.PoolArena 中有一些 field 定義

enum SizeClass { Tiny, // 16B - 496B 16B遞增 Small, // 512B - 4KB 翻倍遞增 Normal // 8KB - 16MB 翻倍遞增 // 還有一個隱藏的,Huge // 16MB - 更大 和申請容量有關}// Tiny類型內存區間:[16,496],內存分配最小為16,每次增加16,直到496,共有31個不同的值static final int numTinySubpagePools = 512 >>> 4;// tinySubpagePools數組的大小,默認:32final int numSmallSubpagePools; // smallSubpagePools數組的大小,默認:pageShifts - 9 = 4private final PoolSubpage[] tinySubpagePools; // tiny 類型的 PoolSubpage 數組,每個元素都是雙向鍊表。容量為32private final PoolSubpage[] smallSubpagePools;// small 類型的 PoolSubpage 數組,每個元素都是雙向鍊表。容量為4final PooledByteBufAllocator parent; // 所屬 PooledByteBufAllocator 對象private final int maxOrder; // 滿二叉樹的高度(從0開始),默認:11final int pageSize; // Page大小,默認:8KBfinal int pageShifts; // 從 1 開始左移到 {@link #pageSize} 的位數。默認 13 ,1 << 13 = 8192final int chunkSize; // Chunk 內存塊佔用大小。8KB * 2048 = 16MBprivate final PoolChunkList q050; // 使用率 50% - 100% 的 Chunk 集合private final PoolChunkList q025; // 使用率 25% - 75% 的 Chunk 集合private final PoolChunkList q000; // 使用率 1% - 50% 的 Chunk 集合private final PoolChunkList qInit; // 使用率 MIN_VALUE - 25% 的 Chunk 集合private final PoolChunkList q075; // 使用率 75% - 100% 的 Chunk 集合private final PoolChunkList q100; // 使用率 100% 的 Chunk 集合// 該 PoolArena 正在被多少線程同時引用final AtomicInteger numThreadCaches = new AtomicInteger;

其中關鍵的數據結構是兩個 PoolSubpage 數組和六個 PoolChunkList 雙向鍊表。

4.2 PoolArena 中的兩個 PoolSubpage 數組

兩個 PoolSubpage 數組正如屬性名 tinySubpagePools、smallSubpagePools,是分別負責小於 8KB 的 tiny 和 small 類型的內存分配。

Tiny 類型內存區間:[16B,496B],從 16B 開始,以 16B 遞增,直到 496B,共有 31 個不同的值,tinySubpagePools 數組的大小為 numTinySubpagePools = 32。申請 tiny 內存時,根據 PoolArena.tinyIdx方法計算出在數組中的 index.

// index:1 -> 31static int tinyIdx(int normCapacity) { return normCapacity >>> 4;}

Small 類型內存區間:512B、1KB、2KB、4KB,有 4 個不同的值,smallSubpagePools 數組的大小為 numSmallSubpagePools = 4。申請 small 內存時,根據 PoolArena.smallIdx方法計算出在數組中的 index.

// index: 0 -> 3static int smallIdx(int normCapacity) { int tableIdx = 0; int i = normCapacity >>> 10; while (i != 0) { i >>>= 1; tableIdx ; } return tableIdx;}

PoolSubpage 對象本身也是個雙向鍊表, tinySubpagePools、smallSubpagePools 兩個數組在初識化時,每個 index 上初始化一個 prev 和 next 都指向自身 head 頭結點。

4.3 PoolArena 中的六個 PoolChunkList

PoolChunkList 用於分配大於等於 8KB 的 normal 類型內存,六個 PoolChunkList 分別存儲不同使用率的 Chunk,並按使用率高低構成一個雙向循環鍊表。

PoolArena 中對於 qInit -> q100 六個 PoolChunkList 的初始化代碼如下:

根據這段代碼,相信大家能在腦海中構建出雙向循環鍊表的簡易畫面:

這裡有幾個問題和大家解釋說明。

1、「六個 PoolChunkList 分別存儲不同使用率的 Chunk」這句話什麼意思?

內存規格劃分中有提及:Chunk 是 Netty 向作業系統申請內存的單位,默認是 chunkSize = 16MB。後續所有的內存分配也都是基於 Chunk 完成。

第一次進行內存分配時,所有的 chunkList 沒有 chunk 可以分配,則新建一個 chunk 進行內存分配,並添加到 qInit 這個 chunkList 中。假如此時又連續申請內存,在某次申請後該 chunk 被分配出去的內存達到或超過了 16MB * 25% = 4MB,則從 qinit 中進去 nextList (即 q000)中。此時繼續申請內存,在某次申請後被分配達到或超過了 q000 的 maxUsage = 50%,則該 chunk 被移動到 q025 中。同理,該 chunk 若被完全分配,最終則會進入 q100 中。

chunk 中被分配的內存使用完進行釋放時,則根據 chunk 當前的使用率和當前所處的 chunkList 的 minUsage 比較。若低於 minUsage ,該 chunk 會從當前的 chunkList 移動至 prevList 的 chunkList 中。直到該 chunk 被分配的內存完全釋放,則會由 q000 移動至其 prevList(即:null),進而該 chunk 被釋放。

六個 PoolChunkList 分別存儲不同使用率的 chunk,根據 chunk 使用率的升高和降低,在六個 PoolChunkList 形成的循環列表中進行晉升和回退。這樣使得使用率接近的 chunk 在同一 PoolChunkList 或接近的 PoolChunkList 中,方便統一管理。

2、qInit 和 q000 為什麼需要設計成兩個類似的 PoolChunkList

qInit 和 q000 這兩個看似似乎有一個有些多餘,其實不然。從設計中可以看出 qInit 的前驅節點是自己,q000 的前驅節點是 null。下面代碼是 PoolChunkList 進行內存釋放的主要流程。

// PoolChunkList.free 釋放內存的方法boolean free(PoolChunk chunk, long handle, ByteBuffer nioBuffer) { chunk.free(handle, nioBuffer); // 釋放內存空間 if (chunk.usage < minUsage) { // chunk 的使用率低於當前 ChunkList 的 minUsage,則從 ChunkList 移除該 Chunk remove(chunk); // 將 chunk 移動到上一個 ChunkList 節點 // Move the PoolChunk down the PoolChunkList linked-list. return move0(chunk); } return true;}// 遞歸private boolean move0(PoolChunk chunk) { if (prevList == null) { return false; } return prevList.move(chunk); }// 從 move0 方法調用,此處便是 preList 的 move 方法private boolean move(PoolChunk chunk) { // 仍小於當前 ChunkList minUsage,繼續該節點的 move0 移除方法 if (chunk.usage < minUsage) { // Move the PoolChunk down the PoolChunkList linked-list. return move0(chunk); } // 執行真正的添加 chunk 到本 ChunkList 的操作 // PoolChunk fits into this PoolChunkList, adding it here. add0(chunk); return true;}

從上述流程中不難分析出:qInit 中的 Chunk 使用率再小也不會小於它的 minUsage = Integer.MIN_VALUE,所以該 ChunkList 中的 Chunk 永遠不會被回收;而 q000 中的 Chunk 使用率一旦小於 minUsage = 1%,內存被完全釋放後則會被回收。

初識化 Chunk 和 25% 內存以內的分配、釋放會讓 Chunk 一直保留在 qInit 以內,避免重複的初始化操作。q000 的作用主要在於某 Chunk 在經歷大起大落的內存分配和回收後,最終回落到 q000 的 ChunkList 內,完全釋放後進行回收,防止永遠駐留在內存中。qInit 和 q000 的搭配使用,使得內存分配和回收更高效。

3、為什麼多個 ChunkList 的使用率會用一段重疊

從圖中就可以得出答案:臨界區的重疊是為了防止在內存分配和回收時兩個緊鄰 ChunkList 頻繁移動,增加資源的消耗。

在日常的研發過程中恰當使用此思想,可以給程序帶來性能的提升。比如說在使用一個 redis 集合準備存儲 100 個元素,超過 100 個元素則需要裁剪到只留 100 個。

假如你的策略是每次向集合中插入元素後,判斷 currentCapacity 是否大於 100,若大於 100 則刪除多餘的元素。那麼第 101 個元素進出集合 10 次,則會多產生 10 次 IO 進行集合的裁剪假如你的策略是:插入元素後,判斷 currentCapacity 是否大於 100 10(buffer) = 110,若大於 110 才將元素個數裁剪為 100。那麼第 101 個元素進出集合 10 次,相當於上個方案會減少 10 次 IO4.4 PoolArena 的分配流程

PoolArena 中 allocate 方法的偽代碼和流程圖精簡後,如下:

private void allocate(PoolThreadCache cache, PooledByteBuf buf, final int reqCapacity) { if(isTinyOrSmall(normCapacity)){ if (isTiny(normCapacity)) { // < 512B // 1.1 優先從 PoolThreadCache 緩存中,分配 tiny 內存塊,若存在則返回 } else { // 512B、1KB、2KB、4KB // 1.1 優先從 PoolThreadCache 緩存中,分配 small 內存塊,若存在則返回 } // 1.2 其次,從 PoolSubpage 鍊表中,分配 Subpage 內存塊,若存在則返回 // 1.3 最後,PoolSubpage 鍊表中分配不到 Subpage 內存塊,所以申請 8KB 的 Normal 內存塊。 // 即:申請一個 Page 節點,佔用其中一塊 Subpage 內存,並進行 PoolSubpage 鍊表初始化 allocateNormal(buf, reqCapacity, normCapacity); … return; } if (normCapacity = 8KB && <= 16MB 時,分配Normal型內存 // 2.1 優先從 PoolThreadCache 緩存中,分配 Normal 內存塊,並初始化到 PooledByteBuf 中。 // 2.2 申請並分配 Normal 內存塊 allocateNormal(buf, reqCapacity, normCapacity); }else{// 大於16MB時,分配 Huge 型內存 // 3.2 申請並分配 Huge 內存塊,newUnpooledChunk(reqCapacity),非池化,用完便回收 allocateHuge(buf, reqCapacity); }}

PoolArena 的 allocate 內存分配流程中出現了 PoolThreadCache、PoolChunkList、PoolChunk 的 allocate 流程,在下文的介紹中會陸續分析到每一"組件"具體的 allocate 流程。

細心的話,在上述偽代碼和流程圖中可以反覆看到一個 allocateNormal 的方法:

private void allocateNormal(PooledByteBuf buf, int reqCapacity, int normCapacity) { // 按照優先級,從5個 ChunkList 中,分配 Normal 內存塊。如果有一分配成功,返回 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)) { return; } // 第一次進行內存分配時,chunkList 沒有 chunk 可以分配內存。新建一個 chunk 進行內存分配,並添加到qInit列表中 // Add a new chunk. PoolChunk c = newChunk(pageSize, maxOrder, pageShifts, chunkSize); // 申請對應的 Normal 內存塊。實際上,如果實際申請的內存大小為 tiny 或 small 類型,PoolChunk.allocate 內部會做特殊處理 boolean success = c.allocate(buf, reqCapacity, normCapacity); assert success; qInit.add(c); }

code 一旦 show 出來,方法內部做了什麼,一目了然。不過衍生出和上一章「六個 PoolChunkList」中 3 個問題類似的一個兄弟問題:

為什麼 ChunkList 分配內存的順序是 q050、q025、q000、qInit、q075 ?

推測原因大致有以下幾點:

q050 中 chunk 的內存使用率為:50%~100%,這大概是個折中的選擇!這樣大部分情況下,chunk 的使用率都會保持在一個較高水平,提高整個應用的內存使用率。並能頻繁的創建和銷毀,造成性能降低為了保持較高的利用率,其次使用 q025 是個不錯的選擇qinit 和 q000 的 chunk 使用率低,但 qInit 中的 chunk 不會被回收,所以 q000 中若存在可分配的 chunk,則優先使用 q000q075 由於內存使用率偏高,在頻繁分配內存的場景下可能導致分配成功率降低,因此放到最低優先級

最後,記住 PoolArena 內部就是這樣一個結構:

文章太長,大家可能忍不了,上篇就到此結束吧。大家再回顧下內容,下篇很快就來

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了