本文基于 Netty 4.1.112.Final 版本進(jìn)行討論
在之前的 Netty 系列中,筆者是以 4.1.56.Final 版本為基礎(chǔ)和大家討論的,那么從本文開始糙麦,筆者將用最新版本 4.1.112.Final 對(duì) Netty 的相關(guān)設(shè)計(jì)展開解析,之所以這么做的原因是 Netty 的內(nèi)存池設(shè)計(jì)一直在不斷地演進(jìn)優(yōu)化铜涉。
在 4.1.52.Final 之前 Netty 內(nèi)存池是基于 jemalloc3 的設(shè)計(jì)思想實(shí)現(xiàn)的,由于在該版本的實(shí)現(xiàn)中,內(nèi)存規(guī)格的粒度設(shè)計(jì)的比較粗咨跌,可能會(huì)引起比較嚴(yán)重的內(nèi)存碎片問題摹恰。所以為了近一步降低內(nèi)存碎片辫继,Netty 在 4.1.52.Final 版本中重新基于 jemalloc4 的設(shè)計(jì)思想對(duì)內(nèi)存池進(jìn)行了重構(gòu),通過將內(nèi)存規(guī)格近一步拆分成更細(xì)的粒度俗慈,以及重新設(shè)計(jì)了內(nèi)存分配算法盡量將內(nèi)存碎片控制在比較小的范圍內(nèi)姑宽。
隨后在 4.1.75.Final 版本中,Netty 為了近一步降低不必要的內(nèi)存消耗闺阱,將 ChunkSize 從原來的 16M 改為了 4M 炮车。而且在默認(rèn)情況下不在為普通的用戶線程提供內(nèi)存池的 Thread Local 緩存。在兼顧性能的前提下酣溃,將不必要的內(nèi)存消耗盡量控制在比較小的范圍內(nèi)瘦穆。
Netty 在后續(xù)的版本迭代中,針對(duì)內(nèi)存池這塊的設(shè)計(jì)赊豌,仍然會(huì)不斷地伴隨著一些小范圍的優(yōu)化扛或,由于這些優(yōu)化點(diǎn)太過細(xì)小,瑣碎碘饼,筆者就不在一一列出熙兔,所以干脆直接以最新版本 4.1.112.Final 來對(duì)內(nèi)存池的設(shè)計(jì)與實(shí)現(xiàn)展開剖析悲伶。
1. 一步一圖推演 Netty 內(nèi)存池總體架構(gòu)設(shè)計(jì)
Netty 內(nèi)存池的整體設(shè)計(jì)相對(duì)來說還是有那么一點(diǎn)點(diǎn)的復(fù)雜,其中涉及到了眾多概念模型住涉,每種模型在架構(gòu)層面上承擔(dān)著不同的職責(zé)麸锉,模型與模型之間又有著千絲萬縷的聯(lián)系,在面對(duì)一個(gè)復(fù)雜的系統(tǒng)設(shè)計(jì)時(shí)舆声,我們還是按照老套路嘿悬,從最簡(jiǎn)單的設(shè)計(jì)開始鲤孵,一步一步的演進(jìn),直到還原出內(nèi)存池的完整樣貌。
因此在本小節(jié)中轻局,筆者的著墨重點(diǎn)是在總體架構(gòu)設(shè)計(jì)層面上怜校,先把內(nèi)存池涉及到的這些眾多概念模型為大家梳理清晰撤逢,但并不會(huì)涉及太復(fù)雜的源碼實(shí)現(xiàn)細(xì)節(jié)窒朋,讓大家有一個(gè)整體完整的認(rèn)識(shí)。有了這個(gè)基礎(chǔ)腋粥,在本文后續(xù)的小節(jié)中晦雨,我們?cè)賮碓敿?xì)討論源碼的實(shí)現(xiàn)細(xì)節(jié)。
首先第一個(gè)登場(chǎng)的模型是 PoolArena 隘冲, 它是內(nèi)存池中最為重要的一個(gè)概念闹瞧,整個(gè)內(nèi)存管理的核心實(shí)現(xiàn)就是在這里完成的。
PoolArena 有兩個(gè)實(shí)現(xiàn)展辞,一個(gè)是 HeapArena奥邮,負(fù)責(zé)池化堆內(nèi)內(nèi)存,另一個(gè)是 DirectArena罗珍,負(fù)責(zé)池化堆外內(nèi)存洽腺。和上篇文章一樣,本文我們的重點(diǎn)還是在 Direct Memory 的池化管理上覆旱,后續(xù)相關(guān)的源碼實(shí)現(xiàn)蘸朋,筆者都是以 DirectArena 進(jìn)行展開。
我們可以直接把 PoolArena 當(dāng)做一個(gè)內(nèi)存池來看待扣唱,當(dāng)線程在申請(qǐng) PooledByteBuf 的時(shí)候都會(huì)到 PoolArena 中去拿藕坯。這樣一來就引入一個(gè)問題,就是系統(tǒng)中有那么多的線程噪沙,而內(nèi)存的申請(qǐng)又是非常頻繁且重要的操作炼彪,這就導(dǎo)致這么多的線程頻繁的去爭(zhēng)搶這一個(gè) PoolArena,相關(guān)鎖的競(jìng)爭(zhēng)程度會(huì)非常激烈曲聂,極大的影響了內(nèi)存分配的速度霹购。
因此 Netty 設(shè)計(jì)了多個(gè) PoolArena 來分?jǐn)偩€程的競(jìng)爭(zhēng),將線程與 PoolArena 進(jìn)行綁定來降低鎖的競(jìng)爭(zhēng)朋腋,提高內(nèi)存分配的并行度齐疙。
PoolArena 的默認(rèn)個(gè)數(shù)為 availableProcessors * 2
, 因?yàn)?Netty 中的 Reactor 線程個(gè)數(shù)默認(rèn)恰好也是 CPU 核數(shù)的兩倍,而內(nèi)存的分配與釋放在 Reactor 線程中是一個(gè)非常高頻的操作旭咽,所以這里將 Reactor 線程與 PoolArena 一對(duì)一綁定起來贞奋,避免 Reactor 線程之間的相互競(jìng)爭(zhēng)。
除此之外穷绵,我們還可以通過 -Dio.netty.allocator.numHeapArenas
以及 -Dio.netty.allocator.numDirectArenas
來調(diào)整系統(tǒng)中 HeapArena 和 DirectArena 的個(gè)數(shù)轿塔。
public class PooledByteBufAllocator {
// 默認(rèn) HeapArena 的個(gè)數(shù)
private static final int DEFAULT_NUM_HEAP_ARENA;
// 默認(rèn) DirectArena 的個(gè)數(shù)
private static final int DEFAULT_NUM_DIRECT_ARENA;
static {
// PoolArena 的默認(rèn)個(gè)數(shù)為 availableProcessors * 2
final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2;
DEFAULT_NUM_HEAP_ARENA = Math.max(0,
SystemPropertyUtil.getInt(
"io.netty.allocator.numHeapArenas",
(int) Math.min(
defaultMinNumArena,
runtime.maxMemory() / defaultChunkSize / 2 / 3)));
DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
SystemPropertyUtil.getInt(
"io.netty.allocator.numDirectArenas",
(int) Math.min(
defaultMinNumArena,
PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));
}
}
但事實(shí)上,系統(tǒng)中的線程不光只有 Reactor 線程這一種仲墨,還有 FastThreadLocalThread 類型的線程勾缭,以及普通 Thread 類型的用戶線程,位于 Reactor 線程之外的 FastThreadLocalThread 目养, UserThread 在運(yùn)行起來之后會(huì)脫離 Reactor 線程自己?jiǎn)为?dú)向 PoolArena 來申請(qǐng)內(nèi)存俩由。
所以無論是什么類型的線程,在它運(yùn)行起來之后癌蚁,當(dāng)?shù)谝淮蜗騼?nèi)存池申請(qǐng)內(nèi)存的時(shí)候幻梯,都會(huì)采用 Round-Robin
的方式與一個(gè)固定的 PoolArena 進(jìn)行綁定,后續(xù)在線程整個(gè)生命周期中的內(nèi)存申請(qǐng)以及釋放等操作都只會(huì)與這個(gè)綁定的 PoolArena 進(jìn)行交互努释。
所以線程與 PoolArena 的關(guān)系是多對(duì)一的關(guān)系碘梢,也就是說一個(gè)線程只能綁定到一個(gè)固定的 PoolArena 上,而一個(gè) PoolArena 卻可以被多個(gè)線程綁定伐蒂。
這樣一來雖然線程與 PoolArena 產(chǎn)生了綁定煞躬,在很大程度上降低了竟?fàn)幫?PoolArena 的激烈程度,但仍然會(huì)存在競(jìng)爭(zhēng)的情況逸邦。那這種微小的競(jìng)爭(zhēng)會(huì)帶來什么影響呢 汰翠?
針對(duì)內(nèi)存池的場(chǎng)景,比如現(xiàn)在有兩個(gè)線程:Thread1 和 Thread2 昭雌,它倆共同綁定到了同一個(gè) PoolArena 上复唤,Thread1 首先向 PoolArena 申請(qǐng)了一個(gè)內(nèi)存塊,并加載到運(yùn)行它的 CPU1 L1 Cache 中烛卧,Thread1 使用完之后將這個(gè)內(nèi)存塊釋放回 PoolArena佛纫。
假設(shè)此時(shí) Thread2 向 PoolArena 申請(qǐng)同樣尺寸的內(nèi)存塊,而且恰好申請(qǐng)到了剛剛被 Thread1 釋放的內(nèi)存塊总放。注意呈宇,此時(shí)這個(gè)內(nèi)存塊已經(jīng)在 CPU1 L1 Cache 中緩存了,運(yùn)行 Thread2 的 CPU2 L1 Cache 中并沒有局雄,這就涉及到了 cacheline 的核間通信(MESI 協(xié)議相關(guān))甥啄,又要耗費(fèi)幾十個(gè)時(shí)鐘周期。
為了極致的性能炬搭,我們能不能做到無鎖化呢 蜈漓?近一步把 cacheline 核間通信的這部分開銷省去穆桂。
這就需要引入內(nèi)存池的第二個(gè)模型 —— PoolThreadCache ,作為線程的 Thread Local 緩存融虽,它用于緩存線程從 PoolArena 中申請(qǐng)到的內(nèi)存塊享完,線程每次申請(qǐng)內(nèi)存的時(shí)候首先會(huì)到 PoolThreadCache 中查看是否已經(jīng)緩存了相應(yīng)尺寸的內(nèi)存塊,如果有有额,則直接從 PoolThreadCache 獲取般又,如果沒有,再到 PoolArena 中去申請(qǐng)巍佑。同理茴迁,線程每次釋放內(nèi)存的時(shí)候,也是先釋放到 PoolThreadCache 中萤衰,而不會(huì)直接釋放回 PoolArena 堕义。
這樣一來,我們通過為每個(gè)線程引入 Thread Local 本地緩存 —— PoolThreadCache腻菇,實(shí)現(xiàn)了內(nèi)存申請(qǐng)與釋放的無鎖化胳螟,同時(shí)也避免了 cacheline 在多核之間的通信開銷,極大地提升了內(nèi)存池的性能筹吐。
但是這樣又會(huì)引來一個(gè)問題糖耸,就是內(nèi)存消耗太大了,系統(tǒng)中有那么多的線程丘薛,如果每個(gè)線程在向 PoolArena 申請(qǐng)內(nèi)存的時(shí)候嘉竟,我們都為它默認(rèn)創(chuàng)建一個(gè) PoolThreadCache 本地緩存的話,這一部分的內(nèi)存消耗將會(huì)特別大洋侨。
因此為了近一步降低內(nèi)存消耗又同時(shí)兼顧內(nèi)存池的性能舍扰,在 Netty 的權(quán)衡之下,默認(rèn)只會(huì)為 Reactor 線程以及 FastThreadLocalThread 類型的線程創(chuàng)建 PoolThreadCache希坚,而普通的用戶線程在默認(rèn)情況下將不再擁有本地緩存边苹。
同時(shí) Netty 也為此提供了一個(gè)配置選項(xiàng) -Dio.netty.allocator.useCacheForAllThreads
, 默認(rèn)為 false 。如果我們將其配置為 true , 那么 Netty 默認(rèn)將會(huì)為系統(tǒng)中的所有線程分配 PoolThreadCache 裁僧。
DEFAULT_USE_CACHE_FOR_ALL_THREADS = SystemPropertyUtil.getBoolean(
"io.netty.allocator.useCacheForAllThreads", false);
好了个束,現(xiàn)在我們已經(jīng)清楚了內(nèi)存池的線程模型,那么接下來大家一定很好奇這個(gè) PoolArena 里面到底長(zhǎng)什么樣子聊疲。 PoolArena 是內(nèi)存池的核心實(shí)現(xiàn)茬底,它里面管理了各種不同規(guī)格的內(nèi)存塊,PoolArena 的整個(gè)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)都是圍繞著這些內(nèi)存塊的管理展開的获洲。所以在拆解 PoolArena 之前阱表,我們需要知道 Netty 內(nèi)存池究竟劃分了哪些規(guī)格的內(nèi)存塊
于是就引入了內(nèi)存池的第三個(gè)模型 —— SizeClasses ,Netty 的內(nèi)存池也是按照內(nèi)存頁(yè) page 進(jìn)行內(nèi)存管理的,不過與 OS 不同的是最爬,在 Netty 中一個(gè) page 的大小默認(rèn)為 8k涉馁,我們可以通過 -Dio.netty.allocator.pageSize
調(diào)整 page 大小,但最低只能調(diào)整到 4k烂叔,而且 pageSize 必須是 2 的次冪谨胞。
// 8k
int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192);
// 4K
private static final int MIN_PAGE_SIZE = 4096;
Netty 內(nèi)存池最小的管理單位是 page , 而內(nèi)存池單次向 OS 申請(qǐng)內(nèi)存的單位是 Chunk固歪,一個(gè) Chunk 的大小默認(rèn)為 4M蒜鸡。Netty 用一個(gè) PoolChunk 的結(jié)構(gòu)來管理這 4M 的內(nèi)存空間。我們可以通過 -Dio.netty.allocator.maxOrder
來調(diào)整 chunkSize 的大欣紊选(默認(rèn)為 4M)逢防,maxOrder 的默認(rèn)值為 9 ,最大值為 14蒲讯。
// 9
int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 9);
// 8196 << 9 = 4M
final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER;
// 1G
private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2);
我們看到 ChunkSize 的大小是由 PAGE_SIZE 和 MAX_ORDER 共同決定的 —— PAGE_SIZE << MAX_ORDER
忘朝,當(dāng) pageSize 為 8K 的時(shí)候,chunkSize 最大不能超過 128M判帮,無論 pageSize 配置成哪種大小局嘁,最大的 chunkSize 不能超過 1G。
Netty 在向 OS 申請(qǐng)到一個(gè) PoolChunk 的內(nèi)存空間(4M)之后晦墙,會(huì)通過 SizeClasses 近一步將這 4M 的內(nèi)存空間切分成 68 種規(guī)格的內(nèi)存塊來進(jìn)行池化管理悦昵。其中最小規(guī)格的內(nèi)存塊為 16 字節(jié),最大規(guī)格的內(nèi)存塊為 4M 晌畅。也就是說但指,Netty 的內(nèi)存池只提供如下 68 種內(nèi)存規(guī)格來讓用戶申請(qǐng)。
除此之外抗楔,Netty 又將這 68 種內(nèi)存規(guī)格分為了三類:
- [16B , 28K] 這段范圍內(nèi)的規(guī)格被劃分為 Small 規(guī)格棋凳。
- [32K , 4M] 這段范圍內(nèi)的規(guī)格被劃分為 Normal 規(guī)格。
- 超過 4M 的內(nèi)存規(guī)格被劃分為 Huge 規(guī)格连躏。
其中 Small 和 Normal 規(guī)格的內(nèi)存塊會(huì)被內(nèi)存池(PoolArena)進(jìn)行池化管理剩岳,Huge 規(guī)格的內(nèi)存塊不會(huì)被內(nèi)存池管理,當(dāng)我們向內(nèi)存池申請(qǐng) Huge 規(guī)格的內(nèi)存塊時(shí)入热,內(nèi)存池是直接向 OS 申請(qǐng)內(nèi)存拍棕,釋放的時(shí)候也是直接釋放回 OS ,內(nèi)存池并不會(huì)緩存這些 Huge 規(guī)格的內(nèi)存塊才顿。
abstract class PoolArena<T> {
enum SizeClass {
Small,
Normal
}
}
那么接下來的問題就是 Small 和 Normal 這兩種規(guī)格的內(nèi)存塊在 PoolArena 中是如何被管理起來的呢 莫湘?前面我們提到,在 Netty 內(nèi)存池中郑气,內(nèi)存管理的基本單位是 Page , 一個(gè) Page 的內(nèi)存規(guī)格是 8K 幅垮,這個(gè)是內(nèi)存管理的基礎(chǔ),而 Small 尾组, Normal 這兩種規(guī)格是在這個(gè)基礎(chǔ)之上進(jìn)行管理的忙芒。
所以我們首先需要弄清楚 Netty 是如何管理這些以 Page 為粒度的內(nèi)存塊的示弓,這就引入了內(nèi)存池的第四個(gè)模型 —— PoolChunk 。PoolChunk 的設(shè)計(jì)參考了 Linux 內(nèi)核中的伙伴系統(tǒng)呵萨,在內(nèi)核中奏属,內(nèi)存管理的基本單位也是 Page(4K),這些 Page 會(huì)按照伙伴的形式被內(nèi)核組織在伙伴系統(tǒng)中潮峦。
內(nèi)核中的伙伴指的是大小相同并且在物理內(nèi)存上連續(xù)的兩個(gè)或者多個(gè) page(個(gè)數(shù)必須是 2 的次冪)囱皿。
如上圖所示,內(nèi)核中伙伴系統(tǒng)的核心數(shù)據(jù)結(jié)構(gòu)就是這個(gè) struct free_area 類型的數(shù)組 —— free_area[MAX_ORDER]忱嘹。
struct zone {
// 伙伴系統(tǒng)的核心數(shù)據(jù)結(jié)構(gòu)
struct free_area free_area[MAX_ORDER];
}
數(shù)組 free_area[MAX_ORDER] 中的索引表示的是分配階 order嘱腥,這個(gè) order 用于指定對(duì)應(yīng) free_area 結(jié)構(gòu)中組織管理的內(nèi)存塊包含多少個(gè) page。比如 free_area[0] 中管理的內(nèi)存塊都是一個(gè)一個(gè)的 Page , free_area[1] 中管理的內(nèi)存塊尺寸是 2 個(gè) Page 拘悦, free_area[10] 中管理的內(nèi)存塊尺寸為 1024 個(gè) Page齿兔。
這些相同尺寸的內(nèi)存塊在 struct free_area 結(jié)構(gòu)中是通過 struct list_head 結(jié)構(gòu)類型的雙向鏈表統(tǒng)一組織起來的。
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
};
struct list_head {
// 雙向鏈表
struct list_head *next, *prev;
};
當(dāng)我們向內(nèi)核申請(qǐng) 2 ^ order 個(gè) Page 的時(shí)候础米,內(nèi)核首先會(huì)到伙伴系統(tǒng)中的 free_area[order] 對(duì)應(yīng)的雙向鏈表 free_list 中查看是否有空閑的內(nèi)存塊分苇,如果有則從 free_list 將內(nèi)存塊摘下并分配出去,如果沒有屁桑,則繼續(xù)向上到 free_area[order + 1] 中去查找医寿,反復(fù)這個(gè)過程,直到在 free_area[order + n] 中的 free_list 鏈表中找到空閑的內(nèi)存塊掏颊。
但是此時(shí)我們?cè)?free_area[order + n] 鏈表中找到的空閑內(nèi)存塊的尺寸是 2 ^ (order + n) 大小糟红,而我們需要的是 2 ^ order 尺寸的內(nèi)存塊,于是內(nèi)核會(huì)將這 2 ^ (order + n) 大小的內(nèi)存塊逐級(jí)減半分裂乌叶,將每一次分裂后的內(nèi)存塊插入到相應(yīng)的 free_area 數(shù)組里對(duì)應(yīng)的 free_list 鏈表中盆偿,并將最后分裂出的 2 ^ order 尺寸的內(nèi)存塊分配給進(jìn)程使用。
假設(shè)我們現(xiàn)在要向下圖中的伙伴系統(tǒng)申請(qǐng)一個(gè) Page (對(duì)應(yīng)的分配階 order = 0)准浴,那么內(nèi)核會(huì)在伙伴系統(tǒng)中首先查看 order = 0 對(duì)應(yīng)的空閑鏈表 free_area[0] 中是否有空閑內(nèi)存塊可供分配事扭。
如果沒有,內(nèi)核則會(huì)根據(jù)前邊介紹的內(nèi)存分配邏輯乐横,繼續(xù)升級(jí)到 free_area[1] , free_area[2] 鏈表中尋找空閑內(nèi)存塊求橄,直到查找到 free_area[3] 發(fā)現(xiàn)有一個(gè)可供分配的內(nèi)存塊。這個(gè)內(nèi)存塊中包含了 8 個(gè) 連續(xù)的空閑 page葡公。
隨后內(nèi)核會(huì)將 free_area[3] 中的這個(gè)空閑內(nèi)存塊從鏈表中摘下罐农,然后減半分裂成兩個(gè)內(nèi)存塊,分裂出來的這兩個(gè)內(nèi)存塊分別包含 4 個(gè) page(分配階 order = 2)催什。將第二個(gè)內(nèi)存塊(圖中綠色部分涵亏,order = 2),插入到 free_rea[2] 鏈表中。
第一個(gè)內(nèi)存塊(圖中黃色部分气筋,order = 2)繼續(xù)減半分裂拆内,分裂出來的這兩個(gè)內(nèi)存塊分別包含 2 個(gè) page(分配階 order = 1)。如上圖中第 4 步所示宠默,前半部分為黃色麸恍,后半部分為紫色。同理按照前邊的分裂邏輯搀矫,內(nèi)核會(huì)將后半部分內(nèi)存塊(紫色部分抹沪,分配階 order = 1)插入到 free_area[1] 鏈表中。
前半部分(圖中黃色部分艾君,order = 1)在上圖中的第 6 步繼續(xù)減半分裂采够,分裂出來的這兩個(gè)內(nèi)存塊分別包含 1 個(gè) page(分配階 order = 0)肄方,前半部分為青色冰垄,后半部分為黃色。后半部分插入到 frea_area[0] 鏈表中权她,前半部分返回給進(jìn)程虹茶,以上就是內(nèi)核中伙伴系統(tǒng)的內(nèi)存分配過程。
下面我們繼續(xù)來回顧一下內(nèi)核伙伴系統(tǒng)的內(nèi)存回收過程隅要,當(dāng)我們向內(nèi)核釋放 2 ^ order 個(gè) Page 的時(shí)候蝴罪,內(nèi)核首先會(huì)檢查 free_area[order] 對(duì)應(yīng)的 free_list 中是否有與我們要釋放的內(nèi)存塊在內(nèi)存地址上連續(xù)的空閑內(nèi)存塊,如果有地址連續(xù)的內(nèi)存塊步清,則將兩個(gè)內(nèi)存塊進(jìn)行合并要门,然后在到上一級(jí) free_area[order + 1] 中繼續(xù)查找是否有空閑內(nèi)存塊與合并之后的內(nèi)存塊在地址上連續(xù),如果有則繼續(xù)重復(fù)上述過程向上合并廓啊,如果沒有欢搜,則將合并之后的內(nèi)存塊插入到 free_area[order + 1] 中。
假設(shè)我們現(xiàn)在需要將一個(gè)編號(hào)為 10 的 Page 釋放回下圖所示的伙伴系統(tǒng)中谴轮,連續(xù)的編號(hào)表示內(nèi)存地址連續(xù)炒瘟。首先內(nèi)核會(huì)在 free_area[0] 中發(fā)現(xiàn)有一個(gè)空閑的內(nèi)存塊 page11 與要釋放的 page10 連續(xù),于是將兩個(gè)連續(xù)的內(nèi)存塊合并第步,合并之后的內(nèi)存塊的分配階 order = 1疮装。
隨后內(nèi)核在 free_area[1] 中發(fā)現(xiàn) page8 和 page9 組成的內(nèi)存塊與 page10 和 page11 合并后的內(nèi)存塊是伙伴,于是繼續(xù)將這兩個(gè)內(nèi)存塊(分配階 order = 1)繼續(xù)合并成一個(gè)新的內(nèi)存塊(分配階 order = 2)粘都。隨后內(nèi)核會(huì)在 free_area[2] 中查找新合并后的內(nèi)存塊伙伴廓推。
接著內(nèi)核在 free_area[2] 中發(fā)現(xiàn) page12,page13翩隧,page14樊展,page15 組成的內(nèi)存塊與 page8,page9,page10滚局,page11 組成的新內(nèi)存塊是伙伴居暖,于是將它們從 free_area[2] 上摘下繼續(xù)合并成一個(gè)新的內(nèi)存塊(分配階 order = 3),隨后內(nèi)核會(huì)在 free_area[3] 中查找新內(nèi)存塊的伙伴藤肢。
但在 free_area[3] 中的內(nèi)存塊(page20 到 page 27)與新合并的內(nèi)存塊(page8 到 page15)雖然大小相同但是物理上并不連續(xù)太闺,所以它們不是伙伴,不能在繼續(xù)向上合并了嘁圈。于是內(nèi)核將 page8 到 pag15 組成的內(nèi)存塊(分配階 order = 3)插入到 free_area[3] 中省骂,整個(gè)伙伴系統(tǒng)回收內(nèi)存的過程如下如所示:
現(xiàn)在我們已經(jīng)清楚了伙伴系統(tǒng)在 Linux 內(nèi)核中的實(shí)現(xiàn),那么同樣是對(duì) Page 的管理,Netty 中的 PoolChunk 也是一樣蔼紧,它的實(shí)現(xiàn)和內(nèi)核中的伙伴系統(tǒng)非常相似碍扔。PoolChunk 也有一個(gè)數(shù)組 runsAvail。
final class PoolChunk<T> implements PoolChunkMetric {
// Netty 的伙伴系統(tǒng)結(jié)構(gòu)
private final IntPriorityQueue[] runsAvail;
}
和內(nèi)核中的 free_area 數(shù)組一樣轧粟,它們里面都保存了不同 Page 級(jí)別的內(nèi)存塊,不一樣的是內(nèi)核中的伙伴系統(tǒng)一共只有 11 個(gè) Page 級(jí)別的內(nèi)存塊尺寸脓魏,分別是: 1 個(gè) Page 兰吟, 2 個(gè) Page , 4 個(gè) Page茂翔,8 個(gè) Page 一直到 1024 個(gè) Page混蔼。內(nèi)存塊的尺寸必須是 2 的次冪個(gè) Page。
Netty 中的伙伴系統(tǒng)一共有 32 個(gè) Page 級(jí)別的內(nèi)存塊尺寸珊燎,這一點(diǎn)我們可以從前面介紹的 SizeClasses 計(jì)算出來的內(nèi)存規(guī)格表看得出來惭嚣。PoolChunk 中管理的這些 Page 級(jí)別的內(nèi)存塊尺寸只要是 Page 的整數(shù)倍就可以,而不是內(nèi)核中要求的 2 的次冪個(gè) Page悔政。
因此 runsAvail 數(shù)組中一共有 32 個(gè)元素晚吞,數(shù)組下標(biāo)就是上圖中的 pageIndex , 數(shù)組類型為 IntPriorityQueue(優(yōu)先級(jí)隊(duì)列)卓箫,數(shù)組中的每一項(xiàng)存儲(chǔ)著所有相同 size 的內(nèi)存塊载矿,這里的 size 就是上圖中 pageIndex 對(duì)應(yīng)的 size 。
比如 runsAvail[0] 中存儲(chǔ)的全部是單個(gè) Page 的內(nèi)存塊烹卒,runsAvail[1] 中存儲(chǔ)的全部是尺寸為 2 個(gè) Page 的內(nèi)存塊闷盔,runsAvail[2] 中存儲(chǔ)的全部是尺寸為 3 個(gè) Page 的內(nèi)存塊,runsAvail[31] 中存儲(chǔ)的是尺寸為 512 個(gè) Page 的內(nèi)存塊旅急。
Netty 中的一個(gè) Page 是 8k
PoolChunk 可以看做是 Netty 中的伙伴系統(tǒng)逢勾,內(nèi)存的申請(qǐng)和釋放過程和內(nèi)核中的伙伴系統(tǒng)非常相似,當(dāng)我們向 PoolChunk 申請(qǐng) Page 級(jí)別的內(nèi)存塊時(shí)藐吮,Netty 首先會(huì)從上面的 Page 規(guī)格表中獲取到內(nèi)存塊尺寸對(duì)應(yīng)的 pageIndex溺拱,然后到 runsAvail[pageIndex] 中去獲取對(duì)應(yīng)尺寸的內(nèi)存塊逃贝。
如果沒有空閑內(nèi)存塊,Netty 的處理方式也是和內(nèi)核一樣迫摔,逐級(jí)向上去找沐扳,直到在 runsAvail[pageIndex + n] 中找到內(nèi)存塊。然后從這個(gè)大的內(nèi)存塊中將我們需要的內(nèi)存塊尺寸切分出來分配句占,剩下的內(nèi)存塊直接插入到對(duì)應(yīng)的 runsAvail[剩下的內(nèi)存塊尺寸 index]
中沪摄,并不會(huì)像內(nèi)核那樣逐級(jí)減半分裂。
PoolChunk 的內(nèi)存塊回收過程則和內(nèi)核一樣纱烘,回收的時(shí)候會(huì)將連續(xù)的內(nèi)存塊合并成更大的杨拐,直到無法合并為止。最后將合并后的內(nèi)存塊插入到對(duì)應(yīng)的 runsAvail[合并后內(nèi)存塊尺寸 index]
中擂啥。
Netty 這里還有一點(diǎn)和內(nèi)核不一樣的是哄陶,內(nèi)核的伙伴系統(tǒng)是使用 struct free_area
結(jié)構(gòu)來組織相同尺寸的內(nèi)存塊,它是一個(gè)雙向鏈表的結(jié)構(gòu)哺壶,每次向內(nèi)核申請(qǐng) Page 的時(shí)候屋吨,都是從 free_list 的頭部獲取內(nèi)存塊。釋放的時(shí)候也是講內(nèi)存塊插入到 free_list 的頭部变骡。這樣一來我們總是可以獲取到剛剛被釋放的內(nèi)存塊离赫,局部性比較好。
但 Netty 的伙伴系統(tǒng)采用的是 IntPriorityQueue 塌碌,一個(gè)優(yōu)先級(jí)隊(duì)列來組織相同尺寸的內(nèi)存塊,它會(huì)按照內(nèi)存地址從低到高組織這些內(nèi)存塊旬盯,我們每次從 IntPriorityQueue 中獲取的都是內(nèi)存地址最低的內(nèi)存塊台妆。Netty 這樣設(shè)計(jì)的目的主要還是為了降低內(nèi)存碎片,犧牲一定的局部性胖翰。
這里犧牲掉局部性是 OK 的接剩,因?yàn)樵?PoolChunk 的設(shè)計(jì)中,Netty 更加注重內(nèi)存碎片的大小萨咳,PoolChunk 主要提供 Page 級(jí)別內(nèi)存塊的申請(qǐng)懊缺,Normal 規(guī)格 —— [32K , 4M] 的內(nèi)存塊就是從 PoolChunk 中直接申請(qǐng)的。為了使 PoolChunk 這段 4M 的內(nèi)存空間中內(nèi)存碎片盡量的少培他,所以我們每次向 PoolChunk 申請(qǐng) Page 級(jí)別內(nèi)存塊的時(shí)候鹃两,總是從低內(nèi)存地址開始有序的申請(qǐng)。
而在 Netty 的應(yīng)用場(chǎng)景中舀凛,往往頻繁申請(qǐng)的都是那些小規(guī)格的內(nèi)存塊俊扳,針對(duì)這種頻繁使用的 Small 規(guī)格的內(nèi)存塊,Netty 在設(shè)計(jì)上就必須要保證局部性猛遍,因?yàn)檫@塊是熱點(diǎn)馋记,所以性能的考量是首位号坡。
而 Normal 規(guī)格的大內(nèi)存塊,往往不會(huì)那么頻繁的申請(qǐng)梯醒,所以在 PoolChunk 的設(shè)計(jì)上宽堆,內(nèi)存碎片的考量是首位。
現(xiàn)在我們知道了 Normal 規(guī)格的內(nèi)存塊是在 PoolChunk 中管理的茸习,而 PoolChunk 的模型設(shè)計(jì)我們也清楚了日麸,那 Small 規(guī)格的內(nèi)存塊在哪里管理呢 ?這就需要引入內(nèi)存池的第五個(gè)模型 —— PoolSubpage 逮光。
還是一樣的套路代箭,遇事不決問內(nèi)核!涕刚! 由于都是針對(duì) Page 級(jí)別內(nèi)存塊的管理嗡综,所以 PoolChunk 的設(shè)計(jì)參考了內(nèi)核的伙伴系統(tǒng),那么針對(duì)小內(nèi)存塊的管理杜漠,PoolSubpage 自然也會(huì)參考內(nèi)核中的 slab cache 极景。所以 PoolSubpage 可以看做是 Netty 中的 slab 。
對(duì)內(nèi)核 slab 的設(shè)計(jì)實(shí)現(xiàn)細(xì)節(jié)感興趣的讀者可以回看下筆者之前專門介紹 slab 的文章 —— 《細(xì)節(jié)拉滿驾茴,80 張圖帶你一步一步推演 slab 內(nèi)存池的設(shè)計(jì)與實(shí)現(xiàn)》盼樟。由于篇幅的關(guān)系,筆者這里就不再詳細(xì)介紹內(nèi)核中的 slab 了锈至,我們直接從 PoolSubpage 這個(gè)模型的設(shè)計(jì)開始聊起晨缴,思想都是一樣的。
通過前面的介紹我們知道峡捡,PoolChunk 承擔(dān)的是 Page 級(jí)別內(nèi)存塊的管理工作击碗,在 Netty 內(nèi)存池的整個(gè)架構(gòu)設(shè)計(jì)上屬于最底層的模型,它是一個(gè)基座们拙,為整個(gè)內(nèi)存池提供最基礎(chǔ)的內(nèi)存分配能力稍途,分配粒度按照 Page 進(jìn)行。
但在 Netty 的實(shí)際應(yīng)用場(chǎng)景中砚婆,往往使用最頻繁的是 Small 規(guī)格的內(nèi)存塊 —— [16B , 28K] 械拍。我們不可能每申請(qǐng)一個(gè) Small 規(guī)格的內(nèi)存塊(比如 16 字節(jié))都要向 PoolChunk 去獲取一個(gè) Page(8K),這樣內(nèi)存資源的浪費(fèi)是非匙岸ⅲ可觀的坷虑。
所以 Netty 借鑒了 Linux 內(nèi)核中 Slab 的設(shè)計(jì)思想,當(dāng)我們第一次申請(qǐng)一個(gè) Small 規(guī)格的內(nèi)存塊時(shí)验夯,Netty 會(huì)首先到 PoolChunk 中申請(qǐng)一個(gè)或者若干個(gè) Page 組成的大內(nèi)存塊(Page 粒度)猖吴,這個(gè)大內(nèi)存塊在 Netty 中的模型就是 PoolSubpage 。然后按照對(duì)應(yīng)的 Small 規(guī)格將這個(gè)大內(nèi)存塊切分成多個(gè)尺寸相同的小內(nèi)存塊緩存在 PoolSubpage 中挥转。
每次申請(qǐng)這個(gè)規(guī)格的內(nèi)存塊時(shí)海蔽,Netty 都會(huì)到對(duì)應(yīng)尺寸的 PoolSubpage 中去獲取共屈,每次釋放這個(gè)規(guī)格的內(nèi)存塊時(shí),Netty 會(huì)直接將其釋放回對(duì)應(yīng)的 PoolSubpage 中党窜。而且每次申請(qǐng) Small 規(guī)格的內(nèi)存塊時(shí)拗引,Netty 都會(huì)優(yōu)先獲取剛剛釋放回 PoolSubpage 的內(nèi)存塊,保證了局部性幌衣。當(dāng) PoolSubpage 中緩存的所有內(nèi)存塊全部被釋放回來后矾削,Netty 就會(huì)將整個(gè) PoolSubpage 重新釋放回 PoolChunk 中。
比如當(dāng)我們首次向 Netty 內(nèi)存池申請(qǐng)一個(gè) 16 字節(jié)的內(nèi)存塊時(shí)豁护,首先會(huì)從 PoolChunk 中申請(qǐng) 1 個(gè) Page(8K)哼凯,然后包裝成 PoolSubpage 。隨后會(huì)將 PoolSubpage 中的這 8K 內(nèi)存空間切分成 512 個(gè) 16 字節(jié)的小內(nèi)存塊楚里。 后續(xù)針對(duì) 16 字節(jié)小內(nèi)存塊的申請(qǐng)和釋放就都只會(huì)和這個(gè) PoolSubpage 打交道了断部。
當(dāng)我們第一次申請(qǐng) 28K 的內(nèi)存塊時(shí),由于它也是 Small 規(guī)格的尺寸班缎,所以按照相同的套路蝴光,Netty 會(huì)首先從 PoolChunk 中申請(qǐng) 7 個(gè) Pages(56K), 然后包裝成 PoolSubpage。隨后會(huì)將 PoolSubpage 中的這 56K 內(nèi)存空間切分成 2 個(gè) 28K 的內(nèi)存塊达址。
PoolSubpage 的尺寸是內(nèi)存塊的尺寸與 PageSize 的最小公倍數(shù)蔑祟。
每當(dāng)一個(gè) PoolSubpage 分配完之后,Netty 就會(huì)重新到 PoolChunk 中申請(qǐng)一個(gè)新的 PoolSubpage 沉唠。這樣一來疆虚,慢慢的,針對(duì)某一種特定的 Small 規(guī)格右冻,就形成了一個(gè) PoolSubpage 鏈表装蓬,這個(gè)鏈表是一個(gè)雙向循環(huán)鏈表,如下圖所示:
在 Netty 中纱扭,每一個(gè) Small 規(guī)格尺寸都會(huì)對(duì)應(yīng)一個(gè)這樣的 PoolSubpage 雙向循環(huán)鏈表,內(nèi)存池中一共設(shè)計(jì)了 39 個(gè) Small 規(guī)格尺寸 —— [16B , 28k]儡遮,所以也就對(duì)應(yīng)了 39 個(gè)這樣的 PoolSubpage 雙向循環(huán)鏈表乳蛾,形成一個(gè) PoolSubpage 鏈表數(shù)組 —— smallSubpagePools,它是內(nèi)存池中管理 Small 規(guī)格內(nèi)存塊的核心數(shù)據(jù)結(jié)構(gòu)鄙币。
abstract class PoolArena<T> {
// 管理 Small 規(guī)格內(nèi)存塊的核心數(shù)據(jù)結(jié)構(gòu)
final PoolSubpage<T>[] smallSubpagePools;
}
smallSubpagePools 數(shù)組的下標(biāo)就是對(duì)應(yīng)的 Small 規(guī)格在 SizeClasses 內(nèi)存規(guī)格表中的 index 肃叶。
這個(gè)設(shè)計(jì)也是參考了內(nèi)核中的 kmalloc 體系,內(nèi)核中的 kmalloc 也是用一個(gè)數(shù)組來組織不同尺寸的 slab , 只不過和 Netty 不同的是十嘿,kmalloc 支持的小內(nèi)存塊尺寸在 8 字節(jié)到 8K 之間因惭。
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
這里的 smallSubpagePools 就相當(dāng)于是內(nèi)核中的 kmalloc绩衷,關(guān)于 kmalloc 的設(shè)計(jì)與實(shí)現(xiàn)細(xì)節(jié)感興趣的讀者可以回看下筆者之前的文章 —— 《深度解讀 Linux 內(nèi)核級(jí)通用內(nèi)存池 —— kmalloc 體系》蹦魔。
好了激率,到現(xiàn)在我們已經(jīng)清楚了,Netty 內(nèi)存池是如何管理 Small 規(guī)格以及 Normal 規(guī)格的內(nèi)存塊了勿决。根據(jù)目前我們掌握的信息和場(chǎng)景可以得出內(nèi)存池 —— PoolArena 的基本骨架乒躺,如下圖所示:
但這還不是 PoolArena 的完整樣貌,如果 PoolArena 中只有一個(gè) PoolChunk 的話肯定是遠(yuǎn)遠(yuǎn)不夠的低缩,因?yàn)?PoolChunk 總會(huì)有全部分配完畢的那一刻嘉冒,這時(shí) Netty 就不得不在次向 OS 申請(qǐng)一個(gè)新的 PoolChunk (4M),這樣一來咆繁,隨著時(shí)間的推移 讳推,PoolArena 中就會(huì)有多個(gè) PoolChunk,那么這些 PoolChunk 在內(nèi)存池中是如何被組織管理的呢 玩般? 這就引入了內(nèi)存池的第六個(gè)模型 —— PoolChunkList 银觅。
PoolChunkList 是一個(gè)雙向鏈表的數(shù)據(jù)結(jié)構(gòu),它用來組織和管理 PoolArena 中的這些 PoolChunk壤短。
但事實(shí)上设拟,對(duì)于 PoolArena 中的這些眾多 PoolChunk 來說,可能不同 PoolChunk 它們的內(nèi)存使用率都是不一樣的久脯,于是 Netty 又近一步根據(jù) PoolChunk 的內(nèi)存使用率設(shè)計(jì)出了 6 個(gè) PoolChunkList 纳胧。每個(gè) PoolChunkList 管理著內(nèi)存使用率在一定范圍內(nèi)的 PoolChunk。
如上圖所示帘撰,PoolArena 中一共有 6 個(gè) PoolChunkList跑慕,分別是:qInit,q000摧找,q025核行,q050,q075蹬耘,q100芝雪。它們之間通過一個(gè)雙向鏈表串聯(lián)在一起,每個(gè) PoolChunkList 管理著內(nèi)存使用率在相同范圍內(nèi)的 PoolChunk :
qInit 顧名思義综苔,當(dāng)一個(gè)新的 PoolChunk 被創(chuàng)建出來之后惩系,它就會(huì)被放到 qInit 中,該 PoolChunkList 管理的 PoolChunk 內(nèi)存使用率在 [0% , 25%) 之間如筛,當(dāng)里邊的 PoolChunk 內(nèi)存使用率大于等于 25% 時(shí)堡牡,就會(huì)被向后移動(dòng)到下一個(gè) q000 中。
q000 管理的 PoolChunk 內(nèi)存使用率在 [1% , 50%) 之間杨刨,當(dāng)里邊的 PoolChunk 內(nèi)存使用率大于等于 50% 時(shí)晤柄,就會(huì)被向后移動(dòng)到下一個(gè) q025 中。當(dāng)里邊的 PoolChunk 內(nèi)存使用率小于 1% 時(shí)妖胀,PoolChunk 就會(huì)被重新釋放回 OS 中芥颈。因?yàn)?ChunkSize 是 4M 惠勒,Netty 內(nèi)存池提供的最小內(nèi)存塊尺寸為 16 字節(jié),當(dāng) PoolChunk 內(nèi)存使用率小于 1% 時(shí)浇借, 其實(shí)內(nèi)存使用率已經(jīng)就是 0% 了捉撮,對(duì)于一個(gè)已經(jīng)全部釋放完的 Empty PoolChunk,就需要釋放回 OS 中妇垢。
q025 管理的 PoolChunk 內(nèi)存使用率在 [25% , 75%) 之間巾遭,當(dāng)里邊的 PoolChunk 內(nèi)存使用率大于等于 75% 時(shí),就會(huì)被向后移動(dòng)到下一個(gè) q050 中闯估。當(dāng)里邊的 PoolChunk 內(nèi)存使用率小于 25% 時(shí)灼舍,就會(huì)被向前移動(dòng)到上一個(gè) q000 中。
q050 管理的 PoolChunk 內(nèi)存使用率在 [50% , 100%) 之間涨薪,當(dāng)里邊的 PoolChunk 內(nèi)存使用率小于 50% 時(shí)骑素,就會(huì)被向前移動(dòng)到上一個(gè) q025 中。當(dāng)里邊的 PoolChunk 內(nèi)存使用率達(dá)到 100% 時(shí)刚夺,直接移動(dòng)到 q100 中献丑。
q075 管理的 PoolChunk 內(nèi)存使用率在 [75% , 100%) 之間,當(dāng)里邊的 PoolChunk 內(nèi)存使用率小于 75% 時(shí)侠姑,就會(huì)被向前移動(dòng)到上一個(gè) q050 中创橄。當(dāng)里邊的 PoolChunk 內(nèi)存使用率達(dá)到 100% 時(shí),直接移動(dòng)到 q100 中莽红。
q100 管理的全部都是內(nèi)存使用率 100 % 的 PoolChunk妥畏,當(dāng)有內(nèi)存釋放回 PoolChunk 之后,才會(huì)向前移動(dòng)到 q075 中安吁。
從以上內(nèi)容中我們可以看出醉蚁,PoolArena 中的每一個(gè) PoolChunkList 都規(guī)定了其中 PoolChunk 的內(nèi)存使用率的上限和下限,當(dāng)某個(gè) PoolChunkList 中的 PoolChunk 內(nèi)存使用率低于規(guī)定的下限時(shí)鬼店,Netty 首先會(huì)將其從當(dāng)前 PoolChunkList 中移除网棍,然后移動(dòng)到前一個(gè) PoolChunkList 中。
當(dāng) PoolChunk 的內(nèi)存使用率達(dá)到規(guī)定的上限時(shí)妇智,Netty 會(huì)將其移動(dòng)到下一個(gè) PoolChunkList 中确沸。但這里有一個(gè)特殊的設(shè)計(jì)不知大家注意到?jīng)]有,就是 q000 它的 prevList 指向 NULL , 也就是說當(dāng) q000 中的 PoolChunk 內(nèi)存使用率低于下限 —— 1% 時(shí)俘陷,這個(gè) PoolChunk 并不會(huì)向前移動(dòng)到 qInit 中,而是會(huì)釋放回 OS 中观谦。
qInit 的 prevList 指向的是它自己拉盾,也就是說,當(dāng) qInit 中的 PoolChunk 內(nèi)存使用率為 0 % 時(shí)豁状,這個(gè) PoolChunk 并不會(huì)釋放回 OS , 反而是繼續(xù)留在 qInit 中捉偏。那為什么 q000 中的 PoolChunk 內(nèi)存使用率低于下限時(shí)會(huì)釋放回 OS 倒得?而 qInit 中的 PoolChunk 反而要繼續(xù)留在 qInit 中呢 ?
PoolArena 中那些剛剛新被創(chuàng)建出來的 PoolChunk 首先會(huì)被 Netty 添加到 qInit 中夭禽,如果該 PoolChunk 的內(nèi)存使用率一直穩(wěn)定在 0% 到 25% 之間的話霞掺,那么它將會(huì)一直停留在 qInit 中,直到內(nèi)存使用率達(dá)到 25% 才會(huì)被移動(dòng)到下一個(gè) q000 中讹躯。
如果內(nèi)存使用不那么頻繁菩彬,PoolChunk 的內(nèi)存使用率會(huì)慢慢的降到 0% , 但是此時(shí)我們不能釋放它潮梯,而是應(yīng)該讓它繼續(xù)留在 qInit 中骗灶,因?yàn)槿绻坏┽尫牛乱淮涡枰獌?nèi)存的時(shí)候還需要在重新創(chuàng)建 PoolChunk秉馏,所以為了避免 PoolChunk 的重復(fù)創(chuàng)建耙旦,我們需要保證內(nèi)存池 PoolArena 中始終至少有一個(gè) PoolChunk 可用。
如果內(nèi)存使用比較頻繁萝究,q000 中的 PoolChunk 內(nèi)存使用率會(huì)慢慢達(dá)到 50% 免都,隨后它會(huì)被移動(dòng)到下一個(gè) q025 中,隨著內(nèi)存使用率越來越高帆竹,達(dá)到 75% 之后绕娘,它又會(huì)被移動(dòng)到 q050 中,隨著內(nèi)存繼續(xù)的頻繁申請(qǐng)馆揉,最終 PoolChunk 被移動(dòng)了 q100 中业舍。
在內(nèi)存頻繁使用的場(chǎng)景下,這個(gè) PoolChunk 大概率會(huì)一直停留在 q050 或者 q075 中升酣,但如果隨著內(nèi)存使用的熱度降低舷暮,PoolChunk 會(huì)慢慢的向前移動(dòng)直到進(jìn)入到 q000 , 這時(shí)如果內(nèi)存還在持續(xù)釋放噩茄,那么這個(gè) PoolChunk 的內(nèi)存使用率慢慢的就會(huì)低于 1% 下面。
這種情況下,Netty 就會(huì)認(rèn)為此時(shí)內(nèi)存的申請(qǐng)并不頻繁绩聘,沒必要讓它一直停留在內(nèi)存池中沥割,直接將它釋放回 OS 就好。用的多了我就多存點(diǎn)凿菩,用的少了我就少存點(diǎn)机杜,減少內(nèi)存池帶來的不必要內(nèi)存消耗。
以上是筆者要為大家介紹的 Netty 針對(duì) PoolChunkList 的第一個(gè)設(shè)計(jì)衅谷,下面我們繼續(xù)來看第二個(gè)設(shè)計(jì)椒拗,當(dāng)我們向內(nèi)存池 PoolArena 申請(qǐng)內(nèi)存的時(shí)候,進(jìn)入到 PoolArena 內(nèi)部之后,就會(huì)發(fā)現(xiàn)蚀苛,我們同時(shí)面對(duì)的是 5 個(gè)都可提供內(nèi)存分配的 PoolChunkList祷蝌,它們分別是 qInit [0% , 25%) 躯肌,q000 [1% , 50%)筹我,q025 [25% , 75%)赂苗,q050 [50% , 100%) ,q075 [75% , 100%) 渗蟹。那我們到底該選擇哪個(gè) PoolChunkList 進(jìn)行內(nèi)存分配呢 块饺? 也就是說這五個(gè) PoolChunkList 的優(yōu)先級(jí)我們?cè)撊绾尉駬?/strong> ?
Netty 選擇的內(nèi)存分配順序是:q050 > q025 > q000 > qInit > q075
, 那為什么這樣設(shè)計(jì)呢 拙徽?
abstract class PoolArena<T> {
// 分配 Page 級(jí)別的內(nèi)存塊
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache threadCache) {
assert lock.isHeldByCurrentThread();
// PoolChunkList 內(nèi)存分配的優(yōu)先級(jí):q050 > q025 > q000 > qInit > q075
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;
}
// 5 個(gè) PoolChunkList 中沒有可用的 PoolChunk刨沦,重新向 OS 申請(qǐng)一個(gè)新的 PoolChunk(4M)
PoolChunk<T> c = newChunk(sizeClass.pageSize, sizeClass.nPSizes, sizeClass.pageShifts, sizeClass.chunkSize);
// 從新的 PoolChunk 中分配內(nèi)存
boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
assert success;
// 將剛剛創(chuàng)建的 PoolChunk 加入到 qInit 中
qInit.add(c);
}
}
這里有四點(diǎn)核心設(shè)計(jì)原則需要考慮:
Netty 需要盡量控制內(nèi)存的消耗,盡可能用少量的 PoolChunk 滿足大量的內(nèi)存分配需求膘怕,避免創(chuàng)建新的 PoolChunk想诅,提高每個(gè) PoolChunk 的內(nèi)存使用率。
而對(duì)于現(xiàn)有的 PoolChunk 來說岛心,Netty 則需要盡量避免將其回收来破,讓它的服務(wù)周期盡可能長(zhǎng)一些。
在此基礎(chǔ)之上忘古,Netty 需要兼顧內(nèi)存分配的性能徘禁。
Netty 需要在內(nèi)存池的整個(gè)生命周期中,從總體上做到讓 PoolArena 中的這些 PoolChunk 盡量均衡地承擔(dān)內(nèi)存分配的工作髓堪,做到雨露均沾送朱。
那么 Netty 采用這樣的分配順序 —— q050 > q025 > q000 > qInit > q075
,如何保證上述四點(diǎn)核心設(shè)計(jì)原則呢 干旁?
首先前面我們已經(jīng)分析過了驶沼,在內(nèi)存頻繁使用的場(chǎng)景中,內(nèi)存池 PoolArena 中的 PoolChunks 大概率會(huì)集中停留在 q050 和 q075 這兩個(gè) PoolChunkList 中争群。由于 q050 和 q075 中集中了大量的 PoolChunks回怜,所以我們肯定會(huì)先從這兩個(gè) PoolChunkList 查找,一下子就能找到一個(gè) PoolChunk换薄,保證了第三點(diǎn)原則 —— 內(nèi)存分配的性能玉雾。
而 q075 中的 PoolChunk 內(nèi)存使用率已經(jīng)很高了,在 75% 到 100% 之間轻要,很可能容量不能滿足內(nèi)存分配的需求導(dǎo)致申請(qǐng)內(nèi)存失敗复旬,所以我們優(yōu)先從 q050 開始。
由于 q050 [50% , 100%) 中同樣集中了大量的 PoolChunks冲泥,優(yōu)先從 q050 開始分配可以做到盡可能的使用現(xiàn)有的 PoolChunk赢底,避免了這些 PoolChunk 由于長(zhǎng)期不被使用而被釋放回 OS , 保證了第二點(diǎn)設(shè)計(jì)原則。
當(dāng) q050 中沒有 PoolChunk 時(shí),同樣是根據(jù)第二點(diǎn)設(shè)計(jì)原則幸冻,Netty 需要盡量?jī)?yōu)先選擇內(nèi)存使用率高的 PoolChunk,所以優(yōu)先從 q025 [25% , 75%) 進(jìn)行分配咳焚。q025 中沒有則優(yōu)先從 q000 [1% , 50%) 中分配洽损,盡量避免 PoolChunk 的回收。
當(dāng) q000 中沒有 PoolChunk 時(shí)革半,那說明此時(shí)內(nèi)存池中的內(nèi)存容量已經(jīng)不太夠了碑定,但是根據(jù)第一點(diǎn)設(shè)計(jì)原則,在這種情況下又官,仍然需要避免創(chuàng)建新的 PoolChunk延刘,所以下一個(gè)優(yōu)先選擇的 PoolChunkList 應(yīng)該是 qInit [0% , 25%) ,而前面我們也介紹過了六敬,Netty 設(shè)計(jì) qInit 的目的就是為了避免頻繁創(chuàng)建不必要的 PoolChunk碘赖。
當(dāng) qInit 沒有 PoolChunk 時(shí),仍然不會(huì)貿(mào)然創(chuàng)建新的 PoolChunk外构,而是到 q075 中去尋找 PoolChunk 普泡。之所以最后才輪到 q075,這是為了保證第四點(diǎn)設(shè)計(jì)原則审编,因?yàn)?q075 中的內(nèi)存使用率已經(jīng)很高了撼班,為了總體上保證 PoolChunk 均衡地承擔(dān)內(nèi)存分配的工作,所有優(yōu)先到其他內(nèi)存使用率相對(duì)較低的 PoolChunkList 中分配垒酬。
以上是筆者要為大家介紹的 Netty 針對(duì) PoolChunkList 的第二個(gè)設(shè)計(jì)砰嘁,下面我們接著來看第三個(gè)設(shè)計(jì)。大家可能注意到勘究,PoolArena 中的這六個(gè) PoolChunkList 在內(nèi)存使用率區(qū)間的設(shè)計(jì)上有很多重疊的部分矮湘,比如內(nèi)存使用率是 30% 的 PoolChunk 既可以在 q000 中也可以在 q025 中,55% 既可以在 q025 中也可以在 q050 中乱顾,Netty 為什么要將 PoolChunkList 的內(nèi)存使用率區(qū)間設(shè)計(jì)成這么多的重疊區(qū)間 板祝? 為什么不設(shè)計(jì)成恰好連續(xù)銜接的區(qū)間呢 ?
我們可以反過來思考一下走净,假如 Netty 將 PoolChunkList 的內(nèi)存使用率區(qū)間設(shè)計(jì)成恰好連續(xù)銜接的區(qū)間券时,那么會(huì)發(fā)生什么情況 ?
我們現(xiàn)在拿 q025 和 q050 這兩個(gè) PoolChunkList 舉例說明伏伯,假設(shè)現(xiàn)在我們將 q025 的內(nèi)存使用率區(qū)間設(shè)計(jì)成 [25% , 50%) , q050 的內(nèi)存使用率區(qū)間設(shè)計(jì)成 [50% , 75%)橘洞,這樣一來,q025 说搅, q050 炸枣, q075 這三個(gè) PoolChunkList 的內(nèi)存使用率區(qū)間的上限和下限就是恰好連續(xù)銜接的了。
那么隨著 PoolChunk 中內(nèi)存的申請(qǐng)與釋放,會(huì)導(dǎo)致 PoolChunk 的內(nèi)存使用率在不斷的發(fā)生變化适肠,假設(shè)現(xiàn)在有一個(gè) PoolChunk 的內(nèi)存使用率是 45% 霍衫,當(dāng)前停留在 q025 中,當(dāng)分配內(nèi)存之后侯养,內(nèi)存使用率上升至 50% 敦跌,那么該 PoolChunk 就需要立即移動(dòng)到 q050 中。
當(dāng)釋放內(nèi)存之后逛揩,這個(gè)剛剛移動(dòng)到 q050 中的 PoolChunk柠傍,它的內(nèi)存使用率下降到 49% ,那么又會(huì)馬不停蹄地移動(dòng)到 q025 辩稽,也就是說只要這個(gè) PoolChunk 的內(nèi)存使用率在 q025 與 q050 的交界處 50% 附近來回徘徊的話惧笛,每次的內(nèi)存申請(qǐng)與釋放都會(huì)導(dǎo)致這個(gè) PoolChunk 在 q025 與 q050 之間不停地來回移動(dòng)。
同樣的道理逞泄,只要一個(gè) PoolChunk 的內(nèi)存使用率在 75% 左右來回徘徊的話患整,那么每次內(nèi)存的申請(qǐng)與釋放也都會(huì)導(dǎo)致這個(gè) PoolChunk 在 q050 與 q075 之間不停地來回移動(dòng),這樣會(huì)造成一定的性能下降炭懊。
但是如果各個(gè) PoolChunkList 之間的內(nèi)存使用率區(qū)間設(shè)計(jì)成重疊區(qū)間的話并级,那么 PoolChunk 的可調(diào)節(jié)范圍就會(huì)很廣,不會(huì)頻繁地在前后不同的 PoolChunkList 之間來回移動(dòng)侮腹。
我們還是拿 q025 [25% , 75%) 和 q050 [50% , 100%) 來舉例說明嘲碧,現(xiàn)在 q025 中有一個(gè)內(nèi)存使用率為 45% 的 PoolChunk , 當(dāng)分配內(nèi)存之后父阻,內(nèi)存使用率上升至 50% 愈涩,該 PoolChunk 仍然會(huì)繼續(xù)停留在 q025 中,后續(xù)隨著內(nèi)存分配的不斷進(jìn)行加矛,當(dāng)內(nèi)存使用率達(dá)到 75% 的時(shí)候才會(huì)移動(dòng)到 q050 中履婉。
還是這個(gè) PoolChunk , 當(dāng)釋放內(nèi)存之后斟览,PoolChunk 的使用率下降到了 70%毁腿,那么它仍然會(huì)停留在 q050 中,后續(xù)隨著內(nèi)存釋放的不斷進(jìn)行苛茂,當(dāng)內(nèi)存使用率低于 50% 的時(shí)候才會(huì)移動(dòng)到 q025 中已烤。這種重疊區(qū)間的設(shè)計(jì)有效的避免了 PoolChunk 頻繁的在兩個(gè) PoolChunkList 之間來回移動(dòng)。
好了妓羊,到現(xiàn)在為止胯究,我們已經(jīng)明白了內(nèi)存池所有的核心組件設(shè)計(jì),基于本小節(jié)中介紹的 6 個(gè)模型:PoolArena躁绸,PoolThreadCache裕循,SizeClasses臣嚣,PoolChunk ,PoolSubpage剥哑,PoolChunkList 硅则。我們可以得出內(nèi)存池的完整架構(gòu)如下圖所示:
2. Netty 內(nèi)存池的創(chuàng)建與初始化
在清楚了內(nèi)存池的總體架構(gòu)設(shè)計(jì)之后,本小節(jié)我們就來看一下整個(gè)內(nèi)存池的骨架是如何被創(chuàng)建出來的星持,Netty 將整個(gè)內(nèi)存池的實(shí)現(xiàn)封裝在 PooledByteBufAllocator 類中抢埋。
public class PooledByteBufAllocator {
public static final PooledByteBufAllocator DEFAULT =
new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());
}
創(chuàng)建內(nèi)存池所需要的幾個(gè)核心參數(shù)我們需要提前了解下:
preferDirect 默認(rèn)為 true , 用于指定該 Allocator 是否偏向于分配 Direct Memory,其值由
PlatformDependent.directBufferPreferred()
方法決定督暂,相關(guān)的判斷邏輯可以回看下 《聊一聊 Netty 數(shù)據(jù)搬運(yùn)工 ByteBuf 體系的設(shè)計(jì)與實(shí)現(xiàn)》 一文中的第三小節(jié)。nHeapArena 穷吮, nDirectArena 用于指定內(nèi)存池中包含的 HeapArena 逻翁, DirectArena 個(gè)數(shù),它們分別用于池化 Heap Memory 以及 Direct Memory 捡鱼。默認(rèn)個(gè)數(shù)分別為
availableProcessors * 2
八回, 可由參數(shù)-Dio.netty.allocator.numHeapArenas
和-Dio.netty.allocator.numDirectArenas
指定。pageSize 默認(rèn)為 8K 驾诈,用于指定內(nèi)存池中的 Page 大小缠诅。可由參數(shù)
-Dio.netty.allocator.pageSize
指定乍迄,但不能低于 4K 管引。maxOrder 默認(rèn)為 9 , 用于指定內(nèi)存池中 PoolChunk 尺寸闯两,默認(rèn) 4M 褥伴,由
pageSize << maxOrder
計(jì)算得出⊙牵可由參數(shù)-Dio.netty.allocator.maxOrder
指定重慢,但不能超過 14 。smallCacheSize 默認(rèn) 256 逊躁, 可由參數(shù)
-Dio.netty.allocator.smallCacheSize
指定似踱,用于表示每一個(gè) small 內(nèi)存規(guī)格尺寸可以在 PoolThreadCache 中緩存的 small 內(nèi)存塊個(gè)數(shù)。normalCacheSize 默認(rèn) 64 稽煤, 可由參數(shù)
-Dio.netty.allocator.normalCacheSize
指定核芽,用于表示每一個(gè) Normal 內(nèi)存規(guī)格尺寸可以在 PoolThreadCache 中緩存的 Normal 內(nèi)存塊個(gè)數(shù)。useCacheForAllThreads 默認(rèn)為 false , 可由參數(shù)
-Dio.netty.allocator.useCacheForAllThreads
指定念脯。用于表示是否為所有線程創(chuàng)建 PoolThreadCache狞洋。directMemoryCacheAlignment 默認(rèn)為 0 ,可由參數(shù)
-Dio.netty.allocator.directMemoryCacheAlignment
指定 绿店, 用于表示內(nèi)存池中內(nèi)存塊尺寸的對(duì)齊粒度吉懊。
private final PoolThreadLocalCache threadCache;
private final int smallCacheSize;
private final int normalCacheSize;
private final int chunkSize;
// 保存所有 DirectArena
private final PoolArena<ByteBuffer>[] directArenas;
public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder,
int smallCacheSize, int normalCacheSize,
boolean useCacheForAllThreads, int directMemoryCacheAlignment) {
// 默認(rèn)偏向于分配 Direct Memory
super(preferDirect);
// 創(chuàng)建 PoolThreadLocalCache 庐橙,后續(xù)用于將線程與 PoolArena 綁定
// 并為線程創(chuàng)建 PoolThreadCache
threadCache = new PoolThreadLocalCache(useCacheForAllThreads);
// PoolThreadCache 中,針對(duì)每一個(gè) Small 規(guī)格的尺寸可以緩存 256 個(gè)內(nèi)存塊
this.smallCacheSize = smallCacheSize;
// PoolThreadCache 中借嗽,針對(duì)每一個(gè) Normal 規(guī)格的尺寸可以緩存 64 個(gè)內(nèi)存塊
this.normalCacheSize = normalCacheSize;
// PoolChunk 的尺寸
// pageSize << maxOrder = 4M
chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);
// 13 态鳖, pageSize 為 8K
int pageShifts = validateAndCalculatePageShifts(pageSize, directMemoryCacheAlignment);
// 依次創(chuàng)建 nDirectArena 個(gè) DirectArena(省略 HeapArena)
if (nDirectArena > 0) {
// 創(chuàng)建 PoolArena 數(shù)組,個(gè)數(shù)為 2 * processors
directArenas = newArenaArray(nDirectArena);
// 劃分內(nèi)存規(guī)格恶导,建立內(nèi)存規(guī)格索引表
final SizeClasses sizeClasses = new SizeClasses(pageSize, pageShifts, chunkSize,
directMemoryCacheAlignment);
// 初始化 PoolArena 數(shù)組
for (int i = 0; i < directArenas.length; i ++) {
// 創(chuàng)建 DirectArena
PoolArena.DirectArena arena = new PoolArena.DirectArena(this, sizeClasses);
// 保存在 directArenas 數(shù)組中
directArenas[i] = arena;
}
} else {
directArenas = null;
}
}
當(dāng)我們明白了內(nèi)存池的總體架構(gòu)之后浆竭,再來看內(nèi)存池的創(chuàng)建過程就會(huì)覺得非常簡(jiǎn)單了,核心點(diǎn)主要有三個(gè):
首先會(huì)創(chuàng)建 PoolThreadLocalCache惨寿,它是一個(gè) FastThreadLocal 類型的成員變量邦泄,主要作用是用于后續(xù)實(shí)現(xiàn)線程與 PoolArena 之間的綁定,并為線程創(chuàng)建本地緩存 PoolThreadCache裂垦。
private final class PoolThreadLocalCache extends FastThreadLocal<PoolThreadCache> {
private final boolean useCacheForAllThreads;
PoolThreadLocalCache(boolean useCacheForAllThreads) {
this.useCacheForAllThreads = useCacheForAllThreads;
}
@Override
protected synchronized PoolThreadCache initialValue() {
實(shí)現(xiàn)線程與 PoolArena 之間的綁定
為線程創(chuàng)建本地緩存 PoolThreadCache
}
}
其次是根據(jù) nDirectArena 的個(gè)數(shù)顺囊,創(chuàng)建 PoolArena 數(shù)組,用于保存內(nèi)存池中所有的 PoolArena蕉拢。
private static <T> PoolArena<T>[] newArenaArray(int size) {
return new PoolArena[size];
}
隨后會(huì)創(chuàng)建 SizeClasses 特碳, Netty 內(nèi)存規(guī)格的劃分就是在這里進(jìn)行的,上一小節(jié)中展示的 Netty 內(nèi)存規(guī)格索引表就是在這里創(chuàng)建的晕换。這一塊的內(nèi)容比較多午乓,筆者放在下一小節(jié)中介紹。
最后根據(jù) SizeClasses 創(chuàng)建 nDirectArena 個(gè) PoolArena 實(shí)例闸准,并依次保存在 directArenas 數(shù)組中益愈。內(nèi)存池的創(chuàng)建核心關(guān)鍵在于創(chuàng)建 PoolArena 結(jié)構(gòu),PoolArena 中管理了 Small 規(guī)格的內(nèi)存塊與 PoolChunk恕汇。其中管理 Small 規(guī)格內(nèi)存塊的數(shù)據(jù)結(jié)構(gòu)是 smallSubpagePools 數(shù)組腕唧,管理 PoolChunk 的數(shù)據(jù)結(jié)構(gòu)是六個(gè) PoolChunkList ,分別按照不同的內(nèi)存使用率進(jìn)行劃分瘾英。
abstract class PoolArena<T> {
// Small 規(guī)格的內(nèi)存塊組織在這里枣接,類似內(nèi)核的 kmalloc
final PoolSubpage<T>[] smallSubpagePools;
// 按照不同內(nèi)存使用率組織 PoolChunk
private final PoolChunkList<T> q050; // [50% , 100%)
private final PoolChunkList<T> q025; // [25% , 75%)
private final PoolChunkList<T> q000; // [1% , 50%)
private final PoolChunkList<T> qInit; // [0% , 25%)
private final PoolChunkList<T> q075; // [75% , 100%)
private final PoolChunkList<T> q100; // 100%
protected PoolArena(PooledByteBufAllocator parent, SizeClasses sizeClass) {
// PoolArena 所屬的 PooledByteBufAllocator
this.parent = parent;
// Netty 內(nèi)存規(guī)格索引表
this.sizeClass = sizeClass;
// small 內(nèi)存規(guī)格將會(huì)在這里分配 —— 類似 kmalloc
// 每一種 small 內(nèi)存規(guī)格都會(huì)對(duì)應(yīng)一個(gè) PoolSubpage 鏈表(類似 slab)
smallSubpagePools = newSubpagePoolArray(sizeClass.nSubpages);
for (int i = 0; i < smallSubpagePools.length; i ++) {
// smallSubpagePools 數(shù)組中的每一項(xiàng)是一個(gè)帶有頭結(jié)點(diǎn)的 PoolSubpage 結(jié)構(gòu)雙向鏈表
// 雙向鏈表的頭結(jié)點(diǎn)是 SubpagePoolHead
smallSubpagePools[i] = newSubpagePoolHead(i);
}
// 按照不同內(nèi)存使用率范圍劃分 PoolChunkList
q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, sizeClass.chunkSize);// [100 , 2147483647]
q075 = new PoolChunkList<T>(this, q100, 75, 100, sizeClass.chunkSize);
q050 = new PoolChunkList<T>(this, q075, 50, 100, sizeClass.chunkSize);
q025 = new PoolChunkList<T>(this, q050, 25, 75, sizeClass.chunkSize);
q000 = new PoolChunkList<T>(this, q025, 1, 50, sizeClass.chunkSize);
qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, sizeClass.chunkSize);// [-2147483648 , 25]
// 雙向鏈表組織 PoolChunkList
// 其中比較特殊的是 q000 的前驅(qū)節(jié)點(diǎn)指向 NULL
// qInit 的前驅(qū)節(jié)點(diǎn)指向它自己
q100.prevList(q075);
q075.prevList(q050);
q050.prevList(q025);
q025.prevList(q000);
q000.prevList(null);
qInit.prevList(qInit);
}
}
首先就是創(chuàng)建 smallSubpagePools 數(shù)組,數(shù)組中的每一個(gè)元素是一個(gè)帶有頭結(jié)點(diǎn)的 PoolSubpage 類型的雙向循環(huán)鏈表結(jié)構(gòu)缺谴,PoolSubpage 類似內(nèi)核中的 slab 但惶,其中管理著對(duì)應(yīng) Small 規(guī)格的小內(nèi)存塊。
Netty 內(nèi)存池中一共設(shè)計(jì)了 39 個(gè) Small 規(guī)格尺寸 —— [16B , 28k]湿蛔,所以 smallSubpagePools 數(shù)組的長(zhǎng)度就是 39 (sizeClass.nSubpages
)膀曾,數(shù)組中的每一項(xiàng)負(fù)責(zé)管理一種 Small 規(guī)格的內(nèi)存塊。
private PoolSubpage<T>[] newSubpagePoolArray(int size) {
return new PoolSubpage[size];
}
smallSubpagePools 數(shù)組中保存就是對(duì)應(yīng) Small 規(guī)格尺寸的 PoolSubpage 鏈表的頭結(jié)點(diǎn) SubpagePoolHead阳啥。在內(nèi)存池剛被創(chuàng)建出來的時(shí)候添谊,鏈表中還是空的,只有一個(gè)頭結(jié)點(diǎn)察迟。
private PoolSubpage<T> newSubpagePoolHead(int index) {
PoolSubpage<T> head = new PoolSubpage<T>(index);
head.prev = head;
head.next = head;
return head;
}
隨后 Netty 會(huì)按照 PoolChunk 的不同內(nèi)存使用率范圍劃分出六個(gè) PoolChunkList :qInit [0% , 25%) 斩狱,q000 [1% , 50%)耳高,q025 [25% , 75%),q050 [50% , 100%) 所踊,q075 [75% , 100%)泌枪,q100 [100%]。它們分別管理著不同內(nèi)存使用率的 PoolChunk秕岛。由于現(xiàn)在內(nèi)存池剛剛被創(chuàng)建出來碌燕,所以這些 PoolChunkList 中還是空的,
這些 PoolChunkLists 通過雙向鏈表的結(jié)構(gòu)相互串聯(lián)起來继薛,其中比較特殊的是 q000 和 qInit修壕。 q000 它的前驅(qū)節(jié)點(diǎn) prevList 指向 NULL ,目的是當(dāng) q000 中的 PoolChunk 內(nèi)存使用率低于 1% 時(shí)遏考,Netty 就會(huì)將其釋放回 OS , 不會(huì)繼續(xù)向前移動(dòng)到 qInit 中叠殷,減少不必要的內(nèi)存消耗。
qInit 它的前驅(qū)節(jié)點(diǎn) prevList 指向它自己诈皿,這么做的目的是,使得內(nèi)存使用率低于 25% 的 PoolChunk 能夠一直停留在 qInit 中像棘,避免后續(xù)需要內(nèi)存的時(shí)候還需要在重新創(chuàng)建 PoolChunk稽亏。
現(xiàn)在一個(gè)完整的內(nèi)存池就被我們創(chuàng)建出來了,但此時(shí)它還只是一個(gè)基本的骨架缕题,內(nèi)存池里面的 PoolArena 還沒有和任何線程進(jìn)行綁定截歉,線程中的本地緩存 PoolThreadCache 還是空的。PoolArena 中的 smallSubpagePools 以及六個(gè) PoolChunkLists 里也都是空的烟零。
在后面的小節(jié)中瘪松,筆者將基于這個(gè)基本的骨架,讓內(nèi)存池動(dòng)態(tài)地運(yùn)轉(zhuǎn)起來锨阿,一步一步豐滿填充里面的內(nèi)容宵睦。但內(nèi)存池運(yùn)轉(zhuǎn)的核心是圍繞著對(duì) Small 規(guī)格以及 Normal 規(guī)格內(nèi)存塊的管理進(jìn)行的。
所以在核心內(nèi)容開始之前墅诡,我們需要知道 Netty 究竟是如何劃分這些不同規(guī)格尺寸的內(nèi)存塊的壳嚎。
3. Netty 內(nèi)存規(guī)格的劃分
如上圖所示,Netty 的內(nèi)存規(guī)格從 16B 到 4M 一共劃分成了 68 種規(guī)格末早,內(nèi)存規(guī)格表在 Netty 中使用了一個(gè)二維數(shù)組來存儲(chǔ)烟馅。
short[][] sizeClasses
其中第一維是按照內(nèi)存規(guī)格的粒度來存儲(chǔ)每一種內(nèi)存規(guī)格,一共有 68 種規(guī)格然磷,一維數(shù)組的大小也是 68 郑趁。第二維存儲(chǔ)的是每一種內(nèi)存規(guī)格的詳細(xì)信息,一共有 7 列姿搜,分別是 index寡润,log2Group捆憎,log2Delta,nDelta悦穿,isMultiPageSize攻礼,isSubpage,log2DeltaLookup栗柒。
private static final int LOG2GROUP_IDX = 1;
private static final int LOG2DELTA_IDX = 2;
private static final int NDELTA_IDX = 3;
private static final int PAGESIZE_IDX = 4;
private static final int SUBPAGE_IDX = 5;
private static final int LOG2_DELTA_LOOKUP_IDX = 6;
其中 index 表示每一種內(nèi)存規(guī)格在 sizeClasses 中的索引礁扮,從 0 到 67 表示 68 種內(nèi)存規(guī)格。
后面的 log2Group 瞬沦,log2Delta太伊,nDelta 都是為了計(jì)算對(duì)應(yīng)的內(nèi)存規(guī)格 size 而設(shè)計(jì)的,計(jì)算公式如下:
private static int calculateSize(int log2Group, int nDelta, int log2Delta) {
return (1 << log2Group) + (nDelta << log2Delta);
}
Netty 按照 log2Group 將內(nèi)存規(guī)格表中的 68 種規(guī)格一共分成了 17 組逛钻,每組 4 個(gè)規(guī)格僚焦,log2Group 用于表示在同一個(gè)內(nèi)存規(guī)格組內(nèi)的 4 個(gè)規(guī)格的基準(zhǔn) size —— base size 的對(duì)數(shù), 后續(xù)規(guī)格 size 將會(huì)在 base size (1 << log2Group)的基礎(chǔ)上進(jìn)行擴(kuò)充。
那么如何擴(kuò)充呢 曙痘?這就用到了 log2Delta 和 nDelta芳悲,每一個(gè)內(nèi)存規(guī)格與其上一個(gè)規(guī)格的差值為 1 << log2Delta
,同一個(gè)內(nèi)存規(guī)格組內(nèi)的規(guī)格相當(dāng)于是一個(gè)等差數(shù)列(log2Delta 都是相同的)边坤。Netty 會(huì)按照 log2Delta 的倍數(shù)對(duì)內(nèi)存規(guī)格進(jìn)行擴(kuò)充名扛,那么擴(kuò)充多少倍呢 ? 這個(gè)就是 nDelta茧痒。
所以一個(gè)內(nèi)存規(guī)格 size 的計(jì)算方式就是基準(zhǔn) size (1 << log2Group) 加上擴(kuò)充的大邪谷汀(nDelta << log2Delta)。下面筆者用第一個(gè)內(nèi)存規(guī)格組 [16B, 32B, 48B , 64B] 進(jìn)行具體說明:
首先第一個(gè)內(nèi)存規(guī)格組的 log2Group 為 4 旺订,它的基準(zhǔn) size = 1 << log2Delta = 16B弄企。log2Delta 為 4 ,表示組內(nèi)的 4 個(gè)內(nèi)存規(guī)格之間的差值為 1 << log2Delta = 16B
区拳。
好了拘领,接下來我們看第一個(gè)內(nèi)存規(guī)格組內(nèi)每一種規(guī)格的計(jì)算方式,Netty 內(nèi)存池的第一個(gè)內(nèi)存規(guī)格是 16B 劳闹, 由于它是第一個(gè)規(guī)格院究,所以 nDelta 為 0 ,我們通過公式 (1 << log2Group) + (nDelta << log2Delta) = (1 << 4) + (0 << 4)
得出第一個(gè)內(nèi)存規(guī)格 size 為 16B本涕。
后續(xù)其他內(nèi)存規(guī)格組內(nèi)第一個(gè)規(guī)格的 nDelta 均是為 1 业汰。
第二個(gè)內(nèi)存規(guī)格是 32B , 它對(duì)應(yīng)的 nDelta 為 1菩颖,規(guī)格 size = (1 << 4) + (1 << 4) = 32
样漆。
第三個(gè)內(nèi)存規(guī)格是 48B , 它對(duì)應(yīng)的 nDelta 為 2晦闰,規(guī)格 size = (1 << 4) + (2 << 4) = 48
放祟。
第四個(gè)內(nèi)存規(guī)格是 64B 鳍怨, 它對(duì)應(yīng)的 nDelta 為 3,規(guī)格 size = (1 << 4) + (3 << 4) = 64
跪妥。
每一個(gè)內(nèi)存規(guī)格組內(nèi)的最后一個(gè)規(guī)格 size 恰好都是 2 的次冪 鞋喇。同時(shí)它也是下一個(gè)內(nèi)存規(guī)格組的 log2Group = log2(size) 。
sizeClasses 中的 isMultiPageSize 表示該內(nèi)存規(guī)格是否是 Page(8k) 的倍數(shù)眉撵,用于后續(xù)索引 Page 級(jí)別的內(nèi)存規(guī)格侦香。isSubpage 表示該內(nèi)存規(guī)格的內(nèi)存塊是否由 PoolSubpage 進(jìn)行管理,從內(nèi)存規(guī)格表 sizeClasses 中我們可以看出纽疟,Small 規(guī)格 [16B , 28k] 范圍內(nèi)的內(nèi)存規(guī)格罐韩,它們的 isSubpage 全都是 true 。
log2DeltaLookup 的用處不大污朽,這里大家可以忽略散吵,4K 以下的內(nèi)存規(guī)格,它們的 log2DeltaLookup 就是 log2Delta蟆肆, 4K 以上的內(nèi)存規(guī)格矾睦,它們的 log2DeltaLookup 都是 0 。這個(gè)設(shè)計(jì)主要是后面用來建立內(nèi)存規(guī)格 size 與其對(duì)應(yīng)的 index 之間的映射索引表 —— size2idxTab炎功。
它的作用就是給定一個(gè)內(nèi)存尺寸 size 中跌,返回其對(duì)應(yīng)的內(nèi)存規(guī)格 index 凤价。內(nèi)存尺寸在 4K 以下直接查找 size2idxTab劝萤,4K 以上通過計(jì)算得出圃伶。這里大家只做簡(jiǎn)單了解即可老充,后面筆者會(huì)介紹這部分的計(jì)算邏輯绣夺。
好了纽门,現(xiàn)在我們已經(jīng)看懂了這張 SizeClasses 內(nèi)存規(guī)格表憨栽,接下來我們就來看一下 SizeClasses 是如何被構(gòu)建出來的酝陈。
final class SizeClasses {
// 第一種內(nèi)存規(guī)格的基準(zhǔn) size —— 16B
// 以及第一個(gè)內(nèi)存規(guī)格增長(zhǎng)間距 —— 16B
static final int LOG2_QUANTUM = 4;
// 每個(gè)內(nèi)存規(guī)格組內(nèi)床玻,規(guī)格的個(gè)數(shù) —— 4 個(gè)
private static final int LOG2_SIZE_CLASS_GROUP = 2;
// size2idxTab 中索引的最大內(nèi)存規(guī)格 —— 4K
private static final int LOG2_MAX_LOOKUP_SIZE = 12;
}
我們?cè)趦?nèi)存規(guī)格表中看到的第一組內(nèi)存規(guī)格基準(zhǔn) size —— 16B , 以及第一種內(nèi)存規(guī)格的增長(zhǎng)間隔 —— 16B 沉帮,就是由 LOG2_QUANTUM 常量決定的锈死。
內(nèi)存規(guī)格表中一共分為了 17 組內(nèi)存規(guī)格,每組包含的規(guī)格個(gè)數(shù)由 LOG2_SIZE_CLASS_GROUP 決定穆壕。
// 22 - 4 -2 + 1 = 17
int group = log2(chunkSize) - LOG2_QUANTUM - LOG2_SIZE_CLASS_GROUP + 1;
size2idxTab 中索引的最大內(nèi)存規(guī)格是由 LOG2_MAX_LOOKUP_SIZE 決定的待牵,如果給定的內(nèi)存尺寸小于等于 1 << LOG2_MAX_LOOKUP_SIZE
,那么直接查找 size2idxTab 獲取其對(duì)應(yīng)的內(nèi)存規(guī)格 index喇勋。內(nèi)存尺寸大于 1 << LOG2_MAX_LOOKUP_SIZE
缨该,則通過計(jì)算得出對(duì)應(yīng)的 index , 不會(huì)建立這部分索引。
SizeClasses(int pageSize, int pageShifts, int chunkSize, int directMemoryCacheAlignment) {
// 一共分為 17 個(gè)內(nèi)存規(guī)格組
int group = log2(chunkSize) - LOG2_QUANTUM - LOG2_SIZE_CLASS_GROUP + 1;
// 創(chuàng)建內(nèi)存規(guī)格表 sizeClasses
// 每個(gè)內(nèi)存規(guī)格組內(nèi)有 4 個(gè)規(guī)格川背,一共 68 個(gè)內(nèi)存規(guī)格贰拿,一維數(shù)組長(zhǎng)度為 68
// 二維數(shù)組的長(zhǎng)度為 7
// 保存的內(nèi)存規(guī)格信息為:index, log2Group, log2Delta, nDelta, isMultiPageSize, isSubPage, log2DeltaLookup
short[][] sizeClasses = new short[group << LOG2_SIZE_CLASS_GROUP][7];
int normalMaxSize = -1;
// 內(nèi)存規(guī)格 index , 初始為 0
int nSizes = 0;
// 內(nèi)存規(guī)格 size
int size = 0;
// 第一組內(nèi)存規(guī)格的基準(zhǔn) size 為 16B
int log2Group = LOG2_QUANTUM;
// 第一組內(nèi)存規(guī)格之間的間隔為 16B
int log2Delta = LOG2_QUANTUM;
// 每個(gè)內(nèi)存規(guī)格組內(nèi)限定為 4 個(gè)規(guī)格
int ndeltaLimit = 1 << LOG2_SIZE_CLASS_GROUP;
// 初始化第一個(gè)內(nèi)存規(guī)格組 [16B , 64B]蛤袒,nDelta 從 0 開始
for (int nDelta = 0; nDelta < ndeltaLimit; nDelta++, nSizes++) {
// 初始化對(duì)應(yīng)內(nèi)存規(guī)格的 7 個(gè)信息
short[] sizeClass = newSizeClass(nSizes, log2Group, log2Delta, nDelta, pageShifts);
// nSizes 為該內(nèi)存規(guī)格的 index
sizeClasses[nSizes] = sizeClass;
// 通過 sizeClass 計(jì)算該內(nèi)存規(guī)格的 size ,然后將 size 向上對(duì)齊至 directMemoryCacheAlignment 的最小整數(shù)倍
size = sizeOf(sizeClass, directMemoryCacheAlignment);
}
// 每個(gè)內(nèi)存規(guī)格組內(nèi)的最后一個(gè)規(guī)格,往往是下一個(gè)內(nèi)存規(guī)格組的基準(zhǔn) size
// 比如第一個(gè)內(nèi)存規(guī)格組內(nèi)最后一個(gè)規(guī)格 64B 膨更, 它是第二個(gè)內(nèi)存規(guī)格組的基準(zhǔn) size
// 4 + 2 = 6妙真,第二個(gè)內(nèi)存規(guī)格組的基準(zhǔn) size 為 64B
log2Group += LOG2_SIZE_CLASS_GROUP;
// 初始化剩下的 16 個(gè)內(nèi)存規(guī)格組
// 后一個(gè)內(nèi)存規(guī)格組的 log2Group,log2Delta 比前一個(gè)內(nèi)存規(guī)格組的 log2Group 荚守,log2Delta 多 1
for (; size < chunkSize; log2Group++, log2Delta++) {
// 每個(gè)內(nèi)存規(guī)格組內(nèi)的 nDelta 從 1 到 4 珍德,最大內(nèi)存規(guī)格不能超過 chunkSize(4M)
for (int nDelta = 1; nDelta <= ndeltaLimit && size < chunkSize; nDelta++, nSizes++) {
// 初始化對(duì)應(yīng)內(nèi)存規(guī)格的 7 個(gè)信息
short[] sizeClass = newSizeClass(nSizes, log2Group, log2Delta, nDelta, pageShifts);
// nSizes 為該內(nèi)存規(guī)格的 index
sizeClasses[nSizes] = sizeClass;
size = normalMaxSize = sizeOf(sizeClass, directMemoryCacheAlignment);
}
}
// 最大內(nèi)存規(guī)格不能超過 chunkSize(4M)
// 超過 4M 就是 Huge 內(nèi)存規(guī)格,直接分配不進(jìn)行池化管理
assert chunkSize == normalMaxSize;
...... 省略 ......
}
本小節(jié)一開始貼出來的那張內(nèi)存規(guī)格表就是通過上面這段代碼創(chuàng)建出來的健蕊,創(chuàng)建邏輯不算太復(fù)雜菱阵。首先創(chuàng)建一個(gè)空的二維數(shù)組 —— sizeClasses,后續(xù)用它來保存內(nèi)存規(guī)格信息缩功。
Netty 將 chunkSize(4M)一共分為了 17 組晴及,每組 4 個(gè)規(guī)格,一共 68 種內(nèi)存規(guī)格嫡锌,所以 sizeClasses 的一維數(shù)組大小就是 68虑稼,保存每一個(gè)內(nèi)存規(guī)格信息。二維數(shù)組大小是 7势木, 也就是上面筆者介紹的 7 種具體的內(nèi)存規(guī)格信息蛛倦。
short[][] sizeClasses = new short[group << LOG2_SIZE_CLASS_GROUP][7];
首先在第一個(gè) for 循環(huán)中初始化第一個(gè)內(nèi)存規(guī)格組,它的起始 log2Group啦桌,log2Delta 為 4溯壶,也就是說第一個(gè)內(nèi)存規(guī)格組內(nèi)的基準(zhǔn) size 為 16B , 組內(nèi)規(guī)格之間的差值為 16B ,由于是第一組內(nèi)存規(guī)格甫男,所以 nDelta 從 0 開始遞增且改。
接著在第二個(gè)雙重 for 循環(huán)中初始化剩下的 16 組內(nèi)存規(guī)格,從 80B 一直到 4M 板驳。Netty 在劃分內(nèi)存規(guī)格的時(shí)候有一個(gè)特點(diǎn)又跛,就是每個(gè)內(nèi)存規(guī)格組內(nèi)最后一個(gè)規(guī)格 size 一定是 2 的次冪,同時(shí)它也是下一個(gè)內(nèi)存規(guī)格組的基準(zhǔn) size 若治。
比如第一個(gè)內(nèi)存規(guī)格組內(nèi)最后一個(gè)規(guī)格為 64B , 那么第二個(gè)內(nèi)存規(guī)格組的 log2Group 就應(yīng)該是 6 慨蓝。也就是從基準(zhǔn) size —— 64B 開始擴(kuò)充組內(nèi)的規(guī)格。
// 4 + 2 = 6
log2Group += LOG2_SIZE_CLASS_GROUP;
除去第一組內(nèi)存規(guī)格之外端幼,我們看到剩下的 16 組內(nèi)存規(guī)格礼烈,后一個(gè)內(nèi)存規(guī)格組內(nèi)的 log2Group 往往比前一個(gè)內(nèi)存規(guī)格組的 log2Group 多 1 。也就是說內(nèi)存規(guī)格組的基準(zhǔn) size 是按照 2 倍遞增婆跑。以 64B , 128B , ...... ,1M , 2M 這樣遞增济丘。
同時(shí)后一個(gè)內(nèi)存規(guī)格組內(nèi)的 log2Delta 往往比前一個(gè)內(nèi)存規(guī)格組的 log2Delta 多 1 。也就是不同內(nèi)存規(guī)格組內(nèi)規(guī)格之間的差值也是按照 2 倍遞增。規(guī)格之間的間距分別按照 16B 摹迷, 32B 疟赊, 64B ,...... , 0.25M , 0.5M 這樣遞增峡碉。但同一內(nèi)存規(guī)格組內(nèi)的差值永遠(yuǎn)都是相同的近哟。
現(xiàn)在我們已經(jīng)清楚了內(nèi)存規(guī)格組的劃分邏輯,那么具體的內(nèi)存規(guī)格信息是如何初始化的呢 鲫寄?這部分邏輯在 newSizeClass 函數(shù)中實(shí)現(xiàn)吉执。
private static short[] newSizeClass(int index, int log2Group, int log2Delta, int nDelta, int pageShifts) {
// 判斷規(guī)格尺寸是否是 Page 的整數(shù)倍
short isMultiPageSize;
if (log2Delta >= pageShifts) {
// 尺寸按照 Page 的倍數(shù)遞增了,那么一定是 Page 的整數(shù)倍
isMultiPageSize = yes;
} else {
int pageSize = 1 << pageShifts;
// size = 1 << log2Group + nDelta * (1 << log2Delta)
int size = calculateSize(log2Group, nDelta, log2Delta);
// 是否能被 pagesize(8k) 整除
isMultiPageSize = size == size / pageSize * pageSize? yes : no;
}
// 規(guī)格尺寸小于 32K 地来,那么就屬于 Small 規(guī)格戳玫,對(duì)應(yīng)的內(nèi)存塊會(huì)被 PoolSubpage 管理
short isSubpage = log2Size < pageShifts + LOG2_SIZE_CLASS_GROUP? yes : no;
// 如果內(nèi)存規(guī)格 size 小于等于 MAX_LOOKUP_SIZE(4K),那么 log2DeltaLookup 為 log2Delta
// 如果內(nèi)存規(guī)格 size 大于 MAX_LOOKUP_SIZE(4K)未斑,則為 0
// Netty 只會(huì)為 4K 以下的內(nèi)存規(guī)格建立 size2idxTab 索引
int log2DeltaLookup = log2Size < LOG2_MAX_LOOKUP_SIZE ||
log2Size == LOG2_MAX_LOOKUP_SIZE && remove == no
? log2Delta : no;
// 初始化內(nèi)存規(guī)格信息
return new short[] {
(short) index, (short) log2Group, (short) log2Delta,
(short) nDelta, isMultiPageSize, isSubpage, (short) log2DeltaLookup
};
}
現(xiàn)在整個(gè)內(nèi)存規(guī)格表就算初始化完了咕宿,后面的工作比較簡(jiǎn)單,就是遍歷內(nèi)存規(guī)格表蜡秽,初始化一些統(tǒng)計(jì)信息府阀,比如:
nPSizes,表示 Page 級(jí)別的內(nèi)存規(guī)格個(gè)數(shù)芽突,一共有 32 個(gè) Page 級(jí)別的內(nèi)存規(guī)格试浙。
nSubpages , 表示 Small 內(nèi)存規(guī)格的個(gè)數(shù)寞蚌,從 16B 到 28K 一共 39 個(gè)
田巴。smallMaxSizeIdx ,最大的 Small 內(nèi)存規(guī)格對(duì)應(yīng)的 index 挟秤。 Small 內(nèi)存規(guī)格中的最大尺寸為 28K 固额,對(duì)應(yīng)的 sizeIndex = 38。
lookupMaxSize 煞聪, 表示 size2idxTab 中索引的最大尺寸為 4K 。
// Small 規(guī)格中最大的規(guī)格尺寸對(duì)應(yīng)的 index (38)
int smallMaxSizeIdx = 0;
// size2idxTab 中最大的 lookup size (4K)
int lookupMaxSize = 0;
// Page 級(jí)別內(nèi)存規(guī)格的個(gè)數(shù)(32)
int nPSizes = 0;
// Small 內(nèi)存規(guī)格的個(gè)數(shù)(39)
int nSubpages = 0;
// 遍歷內(nèi)存規(guī)格表 sizeClasses逝慧,統(tǒng)計(jì) nPSizes 昔脯, nSubpages,smallMaxSizeIdx笛臣,lookupMaxSize
for (int idx = 0; idx < nSizes; idx++) {
short[] sz = sizeClasses[idx];
// 只要 size 可以被 pagesize 整除云稚,那么就屬于 MultiPageSize
if (sz[PAGESIZE_IDX] == yes) {
nPSizes++;
}
// 只要 size 小于 32K 則為 Subpage 的規(guī)格
if (sz[SUBPAGE_IDX] == yes) {
nSubpages++;
// small 內(nèi)存規(guī)格中的最大尺寸 28K ,對(duì)應(yīng)的 sizeIndex = 38
smallMaxSizeIdx = idx;
}
// 內(nèi)存規(guī)格小于等于 4K 的都屬于 lookup size
if (sz[LOG2_DELTA_LOOKUP_IDX] != no) {
// 4K
lookupMaxSize = sizeOf(sz, directMemoryCacheAlignment);
}
}
// 38
this.smallMaxSizeIdx = smallMaxSizeIdx;
// 4086(4K)
this.lookupMaxSize = lookupMaxSize;
// 32
this.nPSizes = nPSizes;
// 39
this.nSubpages = nSubpages;
// 68
this.nSizes = nSizes;
// 8192(8K)
this.pageSize = pageSize;
// 13
this.pageShifts = pageShifts;
// 4M
this.chunkSize = chunkSize;
// 0
this.directMemoryCacheAlignment = directMemoryCacheAlignment;
現(xiàn)在 Netty 中所有的內(nèi)存規(guī)格尺寸就已經(jīng)全部確定下來了沈堡,包括 68 種內(nèi)存規(guī)格静陈,8K 的 PageSize , 4M 的 ChunkSize。接下來最后一項(xiàng)任務(wù)就是根據(jù)原始的內(nèi)存規(guī)格表 sizeClasses 建立相關(guān)的索引表。
// sizeIndex 與 size 之間的映射
this.sizeIdx2sizeTab = newIdx2SizeTab(sizeClasses, nSizes, directMemoryCacheAlignment);
// 根據(jù) sizeClass 生成 page 級(jí)的內(nèi)存規(guī)格表
// pageIndex 到對(duì)應(yīng)的 size 之間的映射
this.pageIdx2sizeTab = newPageIdx2sizeTab(sizeClasses, nSizes, nPSizes, directMemoryCacheAlignment);
// 4k 之內(nèi)鲸拥,給定一個(gè) size 轉(zhuǎn)換為 sizeIndex
this.size2idxTab = newSize2idxTab(lookupMaxSize, sizeClasses);
3.1 sizeIdx2sizeTab
sizeIdx2sizeTab 主要是建立內(nèi)存規(guī)格 index 到對(duì)應(yīng)規(guī)格 size 之間的映射拐格,這里的 index 就是內(nèi)存規(guī)格表 sizeClasses 中的 index 。
private static int[] newIdx2SizeTab(short[][] sizeClasses, int nSizes, int directMemoryCacheAlignment) {
// 68 種內(nèi)存規(guī)格刑赶,映射條目也是 68
int[] sizeIdx2sizeTab = new int[nSizes];
// 遍歷內(nèi)存規(guī)格表捏浊,建立 index 與規(guī)格 size 之間的映射
for (int i = 0; i < nSizes; i++) {
short[] sizeClass = sizeClasses[i];
// size = 1 << log2Group + nDelta * (1 << log2Delta)
sizeIdx2sizeTab[i] = sizeOf(sizeClass, directMemoryCacheAlignment);
}
return sizeIdx2sizeTab;
}
3.2 pageIdx2sizeTab
pageIdx2sizeTab 建立的是 Page 級(jí)別內(nèi)存規(guī)格的索引表,pageIndex 到對(duì)應(yīng) Page 級(jí)內(nèi)存規(guī)格 size 之間的映射撞叨。這里的 pageIndex 從 0 開始一直到 31金踪。
private static int[] newPageIdx2sizeTab(short[][] sizeClasses, int nSizes, int nPSizes,
int directMemoryCacheAlignment) {
// page 級(jí)的內(nèi)存規(guī)格,個(gè)數(shù)為 32
int[] pageIdx2sizeTab = new int[nPSizes];
int pageIdx = 0;
// 遍歷內(nèi)存規(guī)格表牵敷,建立 pageIdx 與對(duì)應(yīng) Page 級(jí)內(nèi)存規(guī)格 size 之間的映射
for (int i = 0; i < nSizes; i++) {
short[] sizeClass = sizeClasses[i];
if (sizeClass[PAGESIZE_IDX] == yes) {
pageIdx2sizeTab[pageIdx++] = sizeOf(sizeClass, directMemoryCacheAlignment);
}
}
return pageIdx2sizeTab;
}
3.3 size2idxTab
size2idxTab 是建立 request size 與內(nèi)存規(guī)格 index 之間的映射關(guān)系胡岔,那什么是 request size 呢 ? 注意這里的 request size 并不是內(nèi)存規(guī)格表中固定的規(guī)格 size , 因?yàn)閮?nèi)存規(guī)格表是 Netty 提前規(guī)劃好的枷餐,對(duì)于用戶來說靶瘸,可能并不知道 Netty 究竟劃分了哪些固定的內(nèi)存規(guī)格,用戶不一定會(huì)按照 Netty 規(guī)定的 size 進(jìn)行內(nèi)存申請(qǐng)尖淘,申請(qǐng)的內(nèi)存尺寸可能是隨意的奕锌。
比如,內(nèi)存規(guī)格表中的前兩個(gè)規(guī)格是 16B 村生, 32B惊暴。但用戶實(shí)際申請(qǐng)的可能是 6B ,8B 趁桃, 29B 辽话, 30B 這樣子的尺寸。
當(dāng)用戶向內(nèi)存池申請(qǐng) 6B 或者 8B 的內(nèi)存塊時(shí)卫病,那么 Netty 就需要找到與其最接近的內(nèi)存規(guī)格油啤,也就是 16B,對(duì)應(yīng)的規(guī)格 index 是 0蟀苛。當(dāng)用戶申請(qǐng) 29B 或者 30B 的內(nèi)存塊時(shí)益咬,與其最接近的內(nèi)存規(guī)格就是 32B , 對(duì)應(yīng)的規(guī)格 index 是 1 帜平。
針對(duì)上面的例子來說幽告,用戶實(shí)際申請(qǐng)的內(nèi)存尺寸就是 request size,在 size2idxTab 中的概念是 lookup size 裆甩。而 size2idxTab 的作用就是建立 lookup size 與其對(duì)應(yīng)內(nèi)存規(guī)格 index 之間的映射冗锁。這樣一來,Netty 就可以通過任意一個(gè) lookup size 迅速找到與其最接近的內(nèi)存規(guī)格了嗤栓。
那么這個(gè)映射如何建立呢 冻河?我們看到 size2idxTab 的結(jié)構(gòu)只是一個(gè) int 型的數(shù)組箍邮,怎么存放 lookup size 與內(nèi)存規(guī)格 index 的映射關(guān)系呢 ?
int[] size2idxTab
說起映射叨叙,我們很容易想起 Hash 表對(duì)吧锭弊,我們可以將內(nèi)存規(guī)格 index 存儲(chǔ)在 size2idxTab 數(shù)組 , size2idxTab 數(shù)組的 index 我們可以設(shè)計(jì)成 lookup size 的 hash code 摔敛。這樣一來廷蓉,給定一個(gè)任意的 lookup size,我們通過一個(gè)哈希函數(shù)計(jì)算出它的 hash code马昙,這個(gè) hash code 也就是 size2idxTab 數(shù)組的 index桃犬,從而通過 size2idxTab[index] 找到映射的內(nèi)存規(guī)格 index。
那么我們?cè)撊绾卧O(shè)計(jì)一個(gè)這樣的哈希函數(shù)呢 行楞? 能不能從 Netty 的內(nèi)存規(guī)格表中找找規(guī)律攒暇,看看有沒有什么靈感、我們知道 Netty 的基礎(chǔ)內(nèi)存規(guī)格為 16B 子房,從 16B 開始先是按照 16B 這樣的間隔開始慢慢擴(kuò)充內(nèi)存規(guī)格形用,隨后依次按照 32B ,64B, 128B , 256B , ...... , 0.5M 這樣 2 的次冪的倍數(shù)間隔逐漸慢慢擴(kuò)充成 68 種內(nèi)存規(guī)格证杭。
// 基礎(chǔ)內(nèi)存規(guī)格
static final int LOG2_QUANTUM = 4;
這 68 種內(nèi)存規(guī)格都是在 16B 的基礎(chǔ)上擴(kuò)充而來的田度,規(guī)格之間的差值也都是 16 的倍數(shù),因此任何一種內(nèi)存規(guī)格一定是 16 的倍數(shù)解愤。根據(jù)這個(gè)特點(diǎn)镇饺,我們將 4M 的內(nèi)存空間按照 16B 這樣的間隔將 lookup size 的尺寸切分為,16B , 32B , 48B , 64B , 80B , ...... 等等這樣的 lookup 尺寸送讲,它們之間的間隔都是 16B奸笤,不會(huì)像內(nèi)存規(guī)格那樣 2 倍 2 倍的遞增。
如果 lookup size 在(0 , 16B] 范圍內(nèi)哼鬓,那么對(duì)應(yīng)的規(guī)格 index 就是 0 监右,內(nèi)存規(guī)格為 16B , 如果 lookup size 在(16B , 32B] 范圍內(nèi),那么對(duì)應(yīng)的規(guī)格 index 就是 1 异希, 內(nèi)存規(guī)格為 32B健盒,如下圖所示這樣以此類推:
按照這樣的規(guī)律,我們就可以設(shè)計(jì)一個(gè)這樣的哈希函數(shù):
lookupSize - 1 >> LOG2_QUANTUM
比如称簿,9B 通過上面的哈希函數(shù)計(jì)算出來的就是 0 扣癣,恰好是內(nèi)存規(guī)格 16B 的 index (0) , 31B 計(jì)算出來的就是 1 ,恰好是內(nèi)存規(guī)格 32B 的 index(1)予跌,100B 計(jì)算出來的就是 6 ,恰好是內(nèi)存規(guī)格 112B 的 index (6) 善茎。
但如果我們像這樣將 ChunkSize(4M) 按照 16B 的間隔進(jìn)行劃分券册,就會(huì)劃分出 262144 個(gè) lookup size 尺寸,這樣就會(huì)導(dǎo)致 size2idxTab 這張索引表非常的大,而且也沒這必要烁焙。
其實(shí)我們只需要為那些使用頻率最高的內(nèi)存規(guī)格范圍建立索引就好了航邢,剩下低頻使用的內(nèi)存規(guī)格我們直接通過計(jì)算得出,不走索引骄蝇。那么究竟為哪些內(nèi)存規(guī)格建立 lookup 索引呢 膳殷?
這就用到了前面介紹的 lookupMaxSize(4K),Netty 只會(huì)為 4K 以下的內(nèi)存規(guī)格建立索引九火,4K 按照 16 的間隔可以劃分出 256 個(gè) lookup size 尺寸赚窃,大小剛好合適,而且都是高頻使用的內(nèi)存規(guī)格岔激。
這樣一來勒极,只要是 4K 以下的任意 lookupSize,Netty 都可以通過 size2idxTab 索引表在 O(1) 的復(fù)雜度下迅速找到與其最接近的內(nèi)存規(guī)格虑鼎。
但在構(gòu)建 size2idxTab 索引的時(shí)候有一個(gè)特殊的點(diǎn)需要注意辱匿,在內(nèi)存規(guī)格表中,規(guī)格 index 7 之后的內(nèi)存規(guī)格之間的差值并不是恰好是 16 炫彩,而是 16 的 2 的次冪倍數(shù)匾七。
比如 sizeIndex 7 和 8 對(duì)應(yīng)的內(nèi)存規(guī)格之間差值是 32 (2 * 16),sizeIndex 11 和 12 對(duì)應(yīng)的內(nèi)存規(guī)格之間差值是 64 (4 * 16)江兢,sizeIndex 26 和 27 對(duì)應(yīng)的內(nèi)存規(guī)格之間差值是 512 (32 * 16)昨忆。
而 size2idxTab 中規(guī)劃的 lookupSize 尺寸是按照 16 遞增的,所以在 sizeIndex 7 和 8 之間划址,我們需要?jiǎng)澐殖鰞蓚€(gè) lookupSize:144 , 160 , 對(duì)應(yīng)的 lookupIndex 是 8 扔嵌, 9 ,它們對(duì)應(yīng)的內(nèi)存規(guī)格都是 160B(sizeIndex = 8)夺颤。
同樣的道理痢缎, sizeIndex 11 和 12 之間,我們需要?jiǎng)澐殖鏊膫€(gè) lookupSize:272 , 288 , 304 , 320 世澜。對(duì)應(yīng)的 lookupIndex 是 16 , 17 , 18 , 19 独旷。它們對(duì)應(yīng)的內(nèi)存規(guī)格都是 320B(sizeIndex = 12)。
sizeIndex 26 和 27 之間需要?jiǎng)澐殖?32 個(gè) lookupSize寥裂,對(duì)應(yīng)的內(nèi)存規(guī)格都是 4K (sizeIndex = 27)嵌洼。
private static int[] newSize2idxTab(int lookupMaxSize, short[][] sizeClasses) {
// size2idxTab 中的 lookupSize 按照 16 依次遞增,最大為 4K
// 因此 size2idxTab 大小為 4K / 16
int[] size2idxTab = new int[lookupMaxSize >> LOG2_QUANTUM];
// lookupIndex
int idx = 0;
// lookupSize
int size = 0;
// 遍歷 4K 以下的內(nèi)存規(guī)格表 sizeClasses封恰,建立 size2idxTab
for (int i = 0; size <= lookupMaxSize; i++) {
int log2Delta = sizeClasses[i][LOG2DELTA_IDX];
// 計(jì)算規(guī)格之間的差值是 16 的幾倍
// 比如 sizeIndex 7 和 8 對(duì)應(yīng)的內(nèi)存規(guī)格之間差值是 32 (2 * 16)
// 那么這兩個(gè)內(nèi)存規(guī)格之間就需要?jiǎng)澐殖?times 個(gè) lookupSize
int times = 1 << log2Delta - LOG2_QUANTUM;
// 構(gòu)建 size2idxTab
while (size <= lookupMaxSize && times-- > 0) {
// lookupIndex 與 sizeIndex 之間的映射
size2idxTab[idx++] = i;
// lookupSize 按照 16 依次遞增
size = idx + 1 << LOG2_QUANTUM;
}
}
return size2idxTab;
}
好了麻养,現(xiàn)在 lookupSize 在 4K 以下,我們可以通過 size2idxTab 找到與其最接近的內(nèi)存規(guī)格诺舔,那么 5K 到 4M 之間的 lookupSize鳖昌,我們又該如何查找其對(duì)應(yīng)的內(nèi)存規(guī)格呢 备畦?
前面筆者提到過,Netty 將 68 種內(nèi)存規(guī)格劃分成了 17 個(gè)內(nèi)存規(guī)格組许昨,內(nèi)存規(guī)格組編號(hào)從 0 到 16 懂盐。每個(gè)內(nèi)存規(guī)格組內(nèi)有四個(gè)規(guī)格。給定一個(gè)任意的 lookupSize糕档,我們首先的思路是不是要確定這個(gè) lookupSize 到底是屬于哪一個(gè)內(nèi)存規(guī)格組 莉恼?然后在確定這個(gè) lookupSize 最接近組內(nèi)第幾個(gè)規(guī)格 ?
現(xiàn)在思路有了速那,下面我們來看第一個(gè)問題俐银,如何確定 lookupSize 究竟屬于哪一個(gè)內(nèi)存規(guī)格組 ?
還記不記得筆者之前反復(fù)強(qiáng)調(diào)過的一個(gè)特性 —— 每個(gè)內(nèi)存規(guī)格組內(nèi)最后一個(gè)規(guī)格都是 2 的次冪琅坡,第 0 個(gè)內(nèi)存規(guī)格組最后一個(gè)規(guī)格是 64B悉患,第 1 個(gè)內(nèi)存規(guī)格組最后一個(gè)規(guī)格是 128B , 第 2 個(gè)是 256B 榆俺, 第 3 個(gè)是 512B售躁,第 4 個(gè)是 1K, ...... 茴晋,第 16 個(gè)是 4M 陪捷。
我們根據(jù)每組最后一個(gè)規(guī)格的尺寸,就可以得到這樣一個(gè)數(shù)列 —— 64 , 128 , 256 , 512 , 1K , ....... , 4M诺擅。這個(gè)數(shù)列有一個(gè)特點(diǎn)就是從 64 開始逐漸按照 2 的次冪倍數(shù)增長(zhǎng)市袖。因此,我們將數(shù)列中的每項(xiàng)除以 64 就得到一個(gè)新的數(shù)列 —— 2^0 , 2^1 , 2^2 , 2^3 , 2^4 , 2^5 .........烁涌。而新數(shù)列中苍碟,每一項(xiàng)的對(duì)數(shù)就是內(nèi)存規(guī)格組的編號(hào)了。
這個(gè)邏輯明確之后撮执,剩下的實(shí)現(xiàn)就很簡(jiǎn)單了微峰,首先我們需要找到 lookupSize 所在內(nèi)存規(guī)格組的最后一個(gè)規(guī)格 , 直接對(duì) lookupSize 向上取最接近的 2 的次冪。
// 組內(nèi)最后一個(gè)內(nèi)存規(guī)格的對(duì)數(shù)
int x = log2((lookupSize << 1) - 1);
在組內(nèi)最后一個(gè)內(nèi)存規(guī)格現(xiàn)在明確了抒钱,我們將它除以 64 蜓肆,然后取商的對(duì)數(shù)就得到了 shift —— 內(nèi)存規(guī)格組編號(hào)。
// lookupSize 所在內(nèi)存規(guī)格組編號(hào)
int shift = x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM)
有了 shift 之后谋币,我們很容易就能確定出組內(nèi)第一個(gè)規(guī)格的 index , 每個(gè)內(nèi)存規(guī)格組內(nèi)有 4 個(gè)規(guī)格仗扬,現(xiàn)在我們是第 shift 個(gè)內(nèi)存規(guī)格組,該組第一個(gè)規(guī)格的 index 就是 shift * 4 蕾额。
// 組內(nèi)第一個(gè)規(guī)格的 index
int group = shift << LOG2_SIZE_CLASS_GROUP;
而每個(gè)內(nèi)存規(guī)格組內(nèi)早芭,規(guī)格之間的間隔都是相同的,通過 x - LOG2_SIZE_CLASS_GROUP - 1
獲取組內(nèi)間隔 log2Delta诅蝶。
// 組內(nèi)規(guī)格間隔
int log2Delta = x - LOG2_SIZE_CLASS_GROUP - 1;
在有了 group 和 log2Delta 之后退个,我們很容易就能確定這個(gè) lookupSize 最接近組內(nèi)第幾個(gè)規(guī)格 —— lookupSize - 1 >> log2Delta & 3
int mod = lookupSize - 1 >> log2Delta & (1 << LOG2_SIZE_CLASS_GROUP) - 1;
最后 group + mod
就是該 lookupSize 對(duì)應(yīng)的內(nèi)存規(guī)格 index 精肃。下面筆者用一個(gè)具體的例子進(jìn)行說明,假設(shè)我們現(xiàn)在要向內(nèi)存池申請(qǐng) 5000B 的內(nèi)存塊帜乞。
5000B 所在內(nèi)存規(guī)格組最后一個(gè)規(guī)格是 8K,8K 除以 64 得到商的對(duì)數(shù)就是 7 筐眷,說明 5000B 這個(gè)內(nèi)存尺寸位于第 7 個(gè)內(nèi)存規(guī)格組內(nèi)黎烈。組內(nèi)第一個(gè)規(guī)格 index
是 28 ,組內(nèi)間距 log2Delta = 10 匀谣。計(jì)算出的 mod 恰好是 0 照棋。也就是說與 5000B 最貼近的內(nèi)存規(guī)格是 5K , 對(duì)應(yīng)的規(guī)格 index 是 28 武翎。
final class SizeClasses {
@Override
public int size2SizeIdx(int size) {
if (size == 0) {
return 0;
}
// Netty 只會(huì)池化 4M 以下的內(nèi)存塊
if (size > chunkSize) {
return nSizes;
}
// 將 lookupSize 與 Alignment 進(jìn)行對(duì)齊
size = alignSizeIfNeeded(size, directMemoryCacheAlignment);
// lookupSize 在 4K 以下直接去 size2idxTab 中去查
if (size <= lookupMaxSize) {
return size2idxTab[size - 1 >> LOG2_QUANTUM];
}
// 向上取 size 最接近的 2 的次冪烈炭,目的是獲取所屬內(nèi)存規(guī)格組的最后一個(gè)規(guī)格尺寸
int x = log2((size << 1) - 1);
// size 所在內(nèi)存規(guī)格組編號(hào),最后一個(gè)規(guī)格尺寸除以 64 得到商的對(duì)數(shù)
int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);
// 內(nèi)存規(guī)格組內(nèi)第一個(gè)規(guī)格 index
int group = shift << LOG2_SIZE_CLASS_GROUP;
// 組內(nèi)規(guī)格之間的間隔
int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;
// size 最貼近組內(nèi)哪一個(gè)規(guī)格
int mod = size - 1 >> log2Delta & (1 << LOG2_SIZE_CLASS_GROUP) - 1;
// 返回對(duì)應(yīng)內(nèi)存規(guī)格 index
return group + mod;
}
}
《談一談 Netty 的內(nèi)存管理 —— 且看 Netty 如何實(shí)現(xiàn) Java 版的 Jemalloc(中)》
《談一談 Netty 的內(nèi)存管理 —— 且看 Netty 如何實(shí)現(xiàn) Java 版的 Jemalloc(下)》