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[] 中的 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 中的六個 PoolChunkListPoolChunkList 用於分配大於等於 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 內部就是這樣一個結構:
文章太長,大家可能忍不了,上篇就到此結束吧。大家再回顧下內容,下篇很快就來
,