新聞中心
Netty 作為一款高性能的 RPC 框架必然涉及到頻繁的內(nèi)存分配銷毀操作。

創(chuàng)新互聯(lián)長(zhǎng)期為近1000家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為武宣企業(yè)提供專業(yè)的成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營(yíng)銷網(wǎng)站建設(shè),武宣網(wǎng)站改版等技術(shù)服務(wù)。擁有十多年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。
如果是在堆上分配內(nèi)存空間將會(huì)觸發(fā)頻繁的 GC,JDK 在 1.4 之后提供的 NIO 也已經(jīng)提供了直接直接分配堆外內(nèi)存空間的能力,但是也僅僅是提供了基本的能力,創(chuàng)建、回收相關(guān)的功能和效率都很簡(jiǎn)陋。
基于此,在堆外內(nèi)存使用方面,Netty 自己實(shí)現(xiàn)了一套創(chuàng)建、回收堆外內(nèi)存池的相關(guān)功能?;诖宋覀円黄饋砜匆幌?Netty 是如何實(shí)現(xiàn)內(nèi)存分配的。
Netty 中的數(shù)據(jù)容器分類
談到數(shù)據(jù)保存肯定要說到內(nèi)存分配,按照存儲(chǔ)空間來劃分,可以分為堆內(nèi)存和堆外內(nèi)存;按照內(nèi)存區(qū)域連貫性來劃分可以分為池化內(nèi)存和非池化內(nèi)存。這些劃分在 Netty 中的實(shí)現(xiàn)接口分別如下。
按照底層存儲(chǔ)空間劃分:
- 堆緩沖區(qū):HeapBuffer
- 直接緩沖區(qū):DirectBuffer
按照是否池化劃分:
- 池化:PooledBuffer
- 非池化:UnPooledBuffer
默認(rèn)使用 PoolDireBuf 類型的內(nèi)存,這些內(nèi)存主要由 PoolArea 管理。另外 Netty 并不是直接對(duì)外暴露這些 API,提供了 Unsafe 類作為出口暴露數(shù)據(jù)分配的相關(guān)操作。
什么是池化?
一般申請(qǐng)內(nèi)存是檢查當(dāng)前內(nèi)存哪里有適合當(dāng)前數(shù)據(jù)塊大小的空閑內(nèi)存塊,如果有就將數(shù)據(jù)保存在當(dāng)前內(nèi)存塊中。
那么池化想做的事情是:既然每次來數(shù)據(jù)都要去找內(nèi)存地址來存,我就先申請(qǐng)一塊內(nèi)存地址,這一塊就是我的專用空間,內(nèi)存分配、回收我全權(quán)管理。
池化解決的問題:內(nèi)存碎片。
內(nèi)碎片:就是申請(qǐng)的地址空間大于真正數(shù)據(jù)使用的內(nèi)存空間。
比如固定申請(qǐng) 1M 的空間作為某個(gè)線程的使用內(nèi)存,但是該線程每次最多只占用 0.5M,那么每次都有 0.5M 的碎片。如果該空間不被有效回收時(shí)間一長(zhǎng)必然存在內(nèi)存空洞。
外碎片:是指多個(gè)內(nèi)存空間合并的時(shí)候發(fā)現(xiàn)不夠分配給待使用的空間大小。
比如有一個(gè) 20byte,13byte 的連續(xù)內(nèi)存空間可以被回收,現(xiàn)在有一個(gè) 48byte 的數(shù)據(jù)塊需要存儲(chǔ),而這兩個(gè)加起來也只有 33byte 的空間,必然不會(huì)被使用到。
如何實(shí)現(xiàn)內(nèi)存池?
①鏈表維護(hù)空閑內(nèi)存地址
最簡(jiǎn)單的就是弄一個(gè)鏈表來維護(hù)當(dāng)前空閑的內(nèi)存空間地址。如果有使用就從鏈表刪除,有釋放就加入鏈表對(duì)應(yīng)位置。
這種方式實(shí)現(xiàn)簡(jiǎn)單,但是搜索和釋放內(nèi)存維護(hù)的難度還是比較大,不太適合。
②定長(zhǎng)內(nèi)存空間分配
維護(hù)兩個(gè)列表,一個(gè)是未分配內(nèi)存列表,一個(gè)是已分配內(nèi)存列表。每個(gè)內(nèi)存塊都是一樣大小,分配時(shí)如果不夠就將多個(gè)塊合并到一起。
這種方式的缺點(diǎn)就是會(huì)浪費(fèi)一定的內(nèi)存空間,如果有特定的場(chǎng)景還是沒有問題。
③多段定長(zhǎng)池分配
在上面的定長(zhǎng)分配基礎(chǔ)上,由原來的固定一個(gè)長(zhǎng)度分配空間變?yōu)榘凑詹煌瑢?duì)象大小(8,16,32,64,128,256,512,1k…64K),的方式分配多個(gè)固定大小的內(nèi)存池。
每次要申請(qǐng)內(nèi)存的時(shí)候按照當(dāng)前對(duì)象大小去對(duì)應(yīng)的池中查找是否有剩余空間。
Linux 本身支持動(dòng)態(tài)內(nèi)存分配和釋放,對(duì)應(yīng)的命令為:malloc/free。malloc 的全稱是 memory allocation,中文叫動(dòng)態(tài)內(nèi)存分配,用于申請(qǐng)一塊連續(xù)的指定大小的內(nèi)存塊區(qū)域以 void* 類型返回分配的內(nèi)存區(qū)域地址。
malloc/free 的實(shí)現(xiàn)過程:
- 空閑存儲(chǔ)空間以空閑鏈表的方式組織(地址遞增),每個(gè)塊包含一個(gè)長(zhǎng)度、一個(gè)指向下一塊的指針以及一個(gè)指向自身存儲(chǔ)空間的指針。(因?yàn)槌绦蛑械哪承┑胤娇赡懿煌ㄟ^ malloc 調(diào)用申請(qǐng),因此 malloc 管理的空間不一定連續(xù))
- 當(dāng)有申請(qǐng)請(qǐng)求時(shí),malloc 會(huì)掃描空閑鏈表,直到找到一個(gè)足夠大的塊為止。(首次適應(yīng))(因此每次調(diào)用 malloc 時(shí)并不是花費(fèi)了完全相同的時(shí)間)
- 如果該塊恰好與請(qǐng)求的大小相符,則將其從鏈表中移走并返回給用戶。如果該塊太大,則將其分為兩部分,尾部的部分分給用戶,剩下的部分留在空閑鏈表中(更改頭部信息)。因此 malloc 分配的是一塊連續(xù)的內(nèi)存。
- 釋放時(shí)首先搜索空閑鏈表,找到可以插入被釋放塊的合適位置。如果與被釋放塊相鄰的任一邊是一個(gè)空閑塊,則將這兩個(gè)塊合為一個(gè)更大的塊,以減少內(nèi)存碎片。
Netty 中的內(nèi)存分配
Netty 采用了 jemalloc 的思想,這是 FreeBSD 實(shí)現(xiàn)的一種并發(fā) malloc 的算法。
jemalloc 依賴多個(gè) Arena(分配器)來分配內(nèi)存,運(yùn)行中的應(yīng)用都有固定數(shù)量的多個(gè) Arena,默認(rèn)的數(shù)量與處理器的個(gè)數(shù)有關(guān)。
系統(tǒng)中有多個(gè) Arena 的原因是由于各個(gè)線程進(jìn)行內(nèi)存分配時(shí)競(jìng)爭(zhēng)不可避免,這可能會(huì)極大的影響內(nèi)存分配的效率,為了緩解高并發(fā)時(shí)的線程競(jìng)爭(zhēng),Netty 允許使用者創(chuàng)建多個(gè)分配器(Arena)來分離鎖,提高內(nèi)存分配效率。
線程首次分配/回收內(nèi)存時(shí),首先會(huì)為其分配一個(gè)固定的 Arena。線程選擇 Arena 時(shí)使用 round-robin 的方式,也就是順序輪流選取。
每個(gè)線程各種保存 Arena 和緩存池信息,這樣可以減少競(jìng)爭(zhēng)并提高訪問效率。Arena 將內(nèi)存分為很多 Chunk 進(jìn)行管理,Chunk 內(nèi)部保存 Page,以頁(yè)為單位申請(qǐng)。
申請(qǐng)內(nèi)存分配時(shí),會(huì)將分配的規(guī)格分為如下四類,分別對(duì)應(yīng)不同的范圍,處理過程也不相同:
- tiny:代表了大小在 0-512B 的內(nèi)存塊。
- small:代表了大小在 512B-8K 的內(nèi)存塊。
- normal:代表了大小在 8K-16M 的內(nèi)存塊。
- huge:代表了大于 16M 的內(nèi)存塊。
每個(gè)塊里面又定義了更細(xì)粒度的單位來分配數(shù)據(jù):
- Chunk:一個(gè) Chunk 的大小是 16M,Chunk 是 Netty 對(duì)操作系統(tǒng)進(jìn)行內(nèi)存申請(qǐng)的單位,后續(xù)所有的內(nèi)存分配都是在 Chunk 里面進(jìn)行操作。
- Page:Chunk 內(nèi)部以 Page 為單位分配內(nèi)存,一個(gè) Page 大小為 8K。當(dāng)我們需要 16K 的空間時(shí),Netty 就會(huì)從一個(gè) Chunk 中找到兩個(gè) Page 進(jìn)行分配。
- Subpage 和 element:element 是比 Page 更小的單位,當(dāng)我們申請(qǐng)小于 8K 的內(nèi)存時(shí),Netty 會(huì)以 element 為單位進(jìn)行內(nèi)存分配。element 沒有固定大小,具體由用戶的需求決定。
Netty 通過 Subpage 管理 element,Subpage 是由 Page 轉(zhuǎn)變過來的。當(dāng)我們需要 1K 的空間時(shí),Netty 會(huì)把一個(gè) Page 變成 Subpage,然后把 Subpage 分成 8 個(gè) 1K 的 element 進(jìn)行分配。
Chunk 中的內(nèi)存分配
線程分配內(nèi)存主要從兩個(gè)地方:PoolThreadCache 和 Arena。其中 PoolThreadCache 線程獨(dú)享,Arena 為幾個(gè)線程共享。
初次申請(qǐng)內(nèi)存的時(shí)候,Netty 會(huì)從一整塊內(nèi)存(Chunk)中分出一部分來給用戶使用,這部分工作是由 Arena 來完成。
而當(dāng)用戶使用完畢釋放內(nèi)存的時(shí)候,這些被分出來的內(nèi)存會(huì)按不同規(guī)格大小放在 PoolThreadCache 中緩存起來。當(dāng)下次要申請(qǐng)內(nèi)存的時(shí)候,就會(huì)先從 PoolThreadCache 中找。
Chunk、Page、Subpage 和 element 都是 Arena 中的概念,Arena 的工作就是從一整塊內(nèi)存中分出合適大小的內(nèi)存塊。
Arena 中最大的內(nèi)存單位是 Chunk,這是 Netty 向操作系統(tǒng)申請(qǐng)內(nèi)存的單位。
而一塊 Chunk(16M) 申請(qǐng)下來之后,內(nèi)部會(huì)被分成 2048 個(gè) Page(8K),當(dāng)用戶向 Netty 申請(qǐng)超過 8K 內(nèi)存的時(shí)候,Netty 會(huì)以 Page 的形式分配內(nèi)存。
Chunk 內(nèi)部通過伙伴算法管理 Page,具體實(shí)現(xiàn)為一棵完全平衡二叉樹:
二叉樹中所有子節(jié)點(diǎn)管理的內(nèi)存也屬于其父節(jié)點(diǎn)。當(dāng)我們要申請(qǐng)大小為 16K 的內(nèi)存時(shí),我們會(huì)從根節(jié)點(diǎn)開始不斷尋找可用的節(jié)點(diǎn),一直到第 10 層。
那么如何判斷一個(gè)節(jié)點(diǎn)是否可用呢?Netty 會(huì)在每個(gè)節(jié)點(diǎn)內(nèi)部保存一個(gè)值,這個(gè)值代表這個(gè)節(jié)點(diǎn)之下的第幾層還存在未分配的節(jié)點(diǎn)。
比如第 9 層的節(jié)點(diǎn)的值如果為 9,就代表這個(gè)節(jié)點(diǎn)本身到下面所有的子節(jié)點(diǎn)都未分配。
如果第 9 層的節(jié)點(diǎn)的值為 10,代表它本身不可被分配,但第 10 層有子節(jié)點(diǎn)可以被分配。
如果第 9 層的節(jié)點(diǎn)的值為 12,此時(shí)可分配節(jié)點(diǎn)的深度大于了總深度,代表這個(gè)節(jié)點(diǎn)及其下面的所有子節(jié)點(diǎn)都不可被分配。
下圖描述了分配的過程:
對(duì)于小內(nèi)存(小于 4096)的分配還會(huì)將 Page 細(xì)化成更小的單位 Subpage。
Subpage 按大小分有兩大類:
- Tiny:小于 512 的情況,最小空間為 16,對(duì)齊大小為 16,區(qū)間為[16,512),所以共有 32 種情況。
- Small:大于等于 512 的情況,總共有四種:512,1024,2048,4096。
PoolSubpage 中直接采用位圖管理空閑空間(因?yàn)椴淮嬖谏暾?qǐng) k 個(gè)連續(xù)的空間),所以申請(qǐng)釋放非常簡(jiǎn)單。
第一次申請(qǐng)小內(nèi)存空間的時(shí)候,需要先申請(qǐng)一個(gè)空閑頁(yè),然后將該頁(yè)轉(zhuǎn)成 PoolSubpage,再將該頁(yè)設(shè)為已被占用,最后再把這個(gè) PoolSubpage 存到 PoolSubpage 池中。
這樣下次就不需要再去申請(qǐng)空閑頁(yè)了,直接去池中找就好了。Netty 中有 36 種 PoolSubpage,所以用 36 個(gè) PoolSubpage 鏈表表示 PoolSubpage 池。
因?yàn)閱蝹€(gè) PoolChunk 只有 16M,這遠(yuǎn)遠(yuǎn)不夠用,所以會(huì)很很多很多 PoolChunk,這些 PoolChunk 組成一個(gè)鏈表,然后用 PoolChunkList 持有這個(gè)鏈表。
我們先從內(nèi)存分配器 PoolArena 來分析 Netty 中的內(nèi)存是如何分配的,Area 的工作就是從一整塊內(nèi)存中協(xié)調(diào)如何分配合適大小的內(nèi)存給當(dāng)前數(shù)據(jù)使用。
PoolArena 是 Netty 的內(nèi)存池實(shí)現(xiàn)抽象類,其內(nèi)部子類為 HeapArena 和 DirectArena。
HeapArena 對(duì)應(yīng)堆內(nèi)存(heap buffer),DirectArena 對(duì)應(yīng)堆外直接內(nèi)存(direct buffer),兩者除了操作的內(nèi)存(byte[] 和 ByteBuffer)不同外其余完全一致。
從結(jié)構(gòu)上來看,PoolArena 中主要包含三部分子內(nèi)存池:
- tinySubpagePools
- smallSubpagePools
- 一系列的 PoolChunkList
tinySubpagePools 和 smallSubpagePools 都是 PoolSubpage 的數(shù)組,數(shù)組長(zhǎng)度分別為 32 和 4。
PoolChunkList 是一個(gè)容器,其內(nèi)部可以保存一系列的 PoolChunk 對(duì)象,并且,Netty 會(huì)根據(jù)內(nèi)存使用率的不同,將 PoolChunkList 分為不同等級(jí)的容器。
- abstract class PoolArena
implements PoolArenaMetric { - enum SizeClass {
- Tiny,
- Small,
- Normal
- }
- // 該參數(shù)指定了tinySubpagePools數(shù)組的長(zhǎng)度,由于tinySubpagePools每一個(gè)元素的內(nèi)存塊差值為16,
- // 因而數(shù)組長(zhǎng)度是512/16,也即這里的512 >>> 4
- static final int numTinySubpagePools = 512 >>> 4;
- //表示該P(yáng)oolArena的allocator
- final PooledByteBufAllocator parent;
- //表示PoolChunk中由Page節(jié)點(diǎn)構(gòu)成的二叉樹的最大高度,默認(rèn)11
- private final int maxOrder;
- //page的大小,默認(rèn)8K
- final int pageSize;
- // 指定了葉節(jié)點(diǎn)大小8KB是2的多少次冪,默認(rèn)為13,該字段的主要作用是,在計(jì)算目標(biāo)內(nèi)存屬于二叉樹的
- // 第幾層的時(shí)候,可以借助于其內(nèi)存大小相對(duì)于pageShifts的差值,從而快速計(jì)算其所在層數(shù)
- final int pageShifts;
- //默認(rèn)16MB
- final int chunkSize;
- // 由于PoolSubpage的大小為8KB=8196,因而該字段的值為
- // -8192=>=> 1111 1111 1111 1111 1110 0000 0000 0000
- // 這樣在判斷目標(biāo)內(nèi)存是否小于8KB時(shí),只需要將目標(biāo)內(nèi)存與該數(shù)字進(jìn)行與操作,只要操作結(jié)果等于0,
- // 就說明目標(biāo)內(nèi)存是小于8KB的,這樣就可以判斷其是應(yīng)該首先在tinySubpagePools或smallSubpagePools
- // 中進(jìn)行內(nèi)存申請(qǐng)
- final int subpageOverflowMask;
- // 該參數(shù)指定了smallSubpagePools數(shù)組的長(zhǎng)度,默認(rèn)為4
- final int numSmallSubpagePools;
- //tinySubpagePools用來分配小于512 byte的Page
- private final PoolSubpage
[] tinySubpagePools; - //smallSubpagePools用來分配大于等于512 byte且小于pageSize內(nèi)存的Page
- private final PoolSubpage
[] smallSubpagePools; - //用來存儲(chǔ)用來分配給大于等于pageSize大小內(nèi)存的PoolChunk
- //存儲(chǔ)內(nèi)存利用率50-100%的chunk
- private final PoolChunkList
q050; - //存儲(chǔ)內(nèi)存利用率25-75%的chunk
- private final PoolChunkList
q025; - //存儲(chǔ)內(nèi)存利用率1-50%的chunk
- private final PoolChunkList
q000; - //存儲(chǔ)內(nèi)存利用率0-25%的chunk
- private final PoolChunkList
qInit; - //存儲(chǔ)內(nèi)存利用率75-100%的chunk
- private final PoolChunkList
q075; - //存儲(chǔ)內(nèi)存利用率100%的chunk
- private final PoolChunkList
q100; - //堆內(nèi)存(heap buffer)
- static final class HeapArena extends PoolArena
{ - }
- //堆外直接內(nèi)存(direct buffer)
- static final class DirectArena extends PoolArena
{ - }
- }
如上所示,PoolArena 是由多個(gè) PoolChunk 組成的大塊內(nèi)存區(qū)域,而每個(gè) PoolChunk 則由多個(gè) Page 組成。
當(dāng)需要分配的內(nèi)存小于 Page 的時(shí)候,為了節(jié)約內(nèi)存采用 PoolSubpage 實(shí)現(xiàn)小于 Page 大小內(nèi)存的分配。
在 PoolArena 中為了保證 PoolChunk 空間的最大利用化,按照 PoolArena 中各 個(gè) PoolChunk 已使用的空間大小將其劃分為六類:
- qInit:存儲(chǔ)內(nèi)存利用率 0-25% 的 chunk
- q000:存儲(chǔ)內(nèi)存利用率 1-50% 的 chunk
- q025:存儲(chǔ)內(nèi)存利用率 25-75% 的 chunk
- q050:存儲(chǔ)內(nèi)存利用率 50-100% 的 chunk
- q075:存儲(chǔ)內(nèi)存利用率 75-100%的 chunk
- q100:存儲(chǔ)內(nèi)存利用率 100%的 chunk
PoolArena 維護(hù)了一個(gè) PoolChunkList 組成的雙向鏈表,每個(gè) PoolChunkList 內(nèi)部維護(hù)了一個(gè) PoolChunk 雙向鏈表。
分配內(nèi)存時(shí),PoolArena 通過在 PoolChunkList 找到一個(gè)合適的 PoolChunk,然后從 PoolChunk 中分配一塊內(nèi)存。
下面來看 PoolArena 是如何分配內(nèi)存的:
- private void allocate(PoolThreadCache cache, PooledByteBuf
buf, final int reqCapacity) { - // 將需要申請(qǐng)的容量格式為 2^N
- final int normCapacity = normalizeCapacity(reqCapacity);
- // 判斷目標(biāo)容量是否小于8KB,小于8KB則使用tiny或small的方式申請(qǐng)內(nèi)存
- if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
- int tableIdx;
- PoolSubpage
[] table; - boolean tiny = isTiny(normCapacity);
- // 判斷目標(biāo)容量是否小于512字節(jié),小于512字節(jié)的為tiny類型的
- if (tiny) { // < 512
- // 將分配區(qū)域轉(zhuǎn)移到 tinySubpagePools 中
- if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
- // was able to allocate out of the cache so move on
- return;
- }
- // 如果無法從當(dāng)前線程緩存中申請(qǐng)到內(nèi)存,則嘗試從tinySubpagePools中申請(qǐng),這里tinyIdx()方法
- // 就是計(jì)算目標(biāo)內(nèi)存是在tinySubpagePools數(shù)組中的第幾號(hào)元素中的
- tableIdx = tinyIdx(normCapacity);
- table = tinySubpagePools;
- } else {
- // 如果目標(biāo)內(nèi)存在512byte~8KB之間,則嘗試從smallSubpagePools中申請(qǐng)內(nèi)存。這里首先從
- // 當(dāng)前線程的緩存中申請(qǐng)small級(jí)別的內(nèi)存,如果申請(qǐng)到了,則直接返回
- if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
- // was able to allocate out of the cache so move on
- return;
- }
- tableIdx = smallIdx(normCapacity);
- table = smallSubpagePools;
- }
- // 獲取目標(biāo)元素的頭結(jié)點(diǎn)
- final PoolSubpage
head = table[tableIdx]; - // 這里需要注意的是,由于對(duì)head進(jìn)行了加鎖,而在同步代碼塊中判斷了s != head,
- // 也就是說PoolSubpage鏈表中是存在未使用的PoolSubpage的,因?yàn)槿绻摴?jié)點(diǎn)已經(jīng)用完了,
- // 其是會(huì)被移除當(dāng)前鏈表的。也就是說只要s != head,那么這里的allocate()方法
- // 就一定能夠申請(qǐng)到所需要的內(nèi)存塊
- synchronized (head) {
- // s != head就證明當(dāng)前PoolSubpage鏈表中存在可用的PoolSubpage,并且一定能夠申請(qǐng)到內(nèi)存,
- // 因?yàn)橐呀?jīng)耗盡的PoolSubpage是會(huì)從鏈表中移除的
- final PoolSubpage
s = head.next; - // 如果此時(shí) subpage 已經(jīng)被分配過內(nèi)存了執(zhí)行下文,如果只是初始化過,則跳過該分支
- if (s != head) {
- // 從PoolSubpage中申請(qǐng)內(nèi)存
- assert s.doNotDestroy && s.elemSize == normCapacity;
- // 通過申請(qǐng)的內(nèi)存對(duì)ByteBuf進(jìn)行初始化
- long handle = s.allocate();
- assert handle >= 0;
- // 初始化 PoolByteBuf 說明其位置被分配到該區(qū)域,但此時(shí)尚未分配內(nèi)存
- s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
- // 對(duì)tiny類型的申請(qǐng)數(shù)進(jìn)行更新
- if (tiny) {
- allocationsTiny.increment();
- } else {
- allocationsSmall.increment();
- }
- return;
- }
- }
- // 走到這里,說明目標(biāo)PoolSubpage鏈表中無法申請(qǐng)到目標(biāo)內(nèi)存塊,因而就嘗試從PoolChunk中申請(qǐng)
- allocateNormal(buf, reqCapacity, normCapacity);
- return;
- }
- // 走到這里說明目標(biāo)內(nèi)存是大于8KB的,那么就判斷目標(biāo)內(nèi)存是否大于16M,如果大于16M,
- // 則不使用內(nèi)存池對(duì)其進(jìn)行管理,如果小于16M,則到PoolChunkList中進(jìn)行內(nèi)存申請(qǐng)
- if (normCapacity <= chunkSize) {
- // 小于16M,首先到當(dāng)前線程的緩存中申請(qǐng),如果申請(qǐng)到了則直接返回,如果沒有申請(qǐng)到,
- // 則到PoolChunkList中進(jìn)行申請(qǐng)
- if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
- // was able to allocate out of the cache so move on
- return;
- }
- allocateNormal(buf, reqCapacity, normCapacity);
- } else {
- // 對(duì)于大于16M的內(nèi)存,Netty不會(huì)對(duì)其進(jìn)行維護(hù),而是直接申請(qǐng),然后返回給用戶使用
- allocateHuge(buf, reqCapacity);
- }
- }
所有內(nèi)存分配的 size 都會(huì)經(jīng)過 normalizeCapacity() 進(jìn)行處理,申請(qǐng)的容量總是會(huì)被格式為 2^N。
主要規(guī)則如下:
- 如果目標(biāo)容量小于 16 字節(jié),則返回 16。
- 如果目標(biāo)容量大于 16 字節(jié),小于 512 字節(jié),則以 16 字節(jié)為單位,返回大于目標(biāo)字節(jié)數(shù)的第一個(gè) 16 字節(jié)的倍數(shù)。比如申請(qǐng)的 100 字節(jié),那么大于 100 的 16 整數(shù)倍最低為:16*7=112,因而返回 112。
- 如果目標(biāo)容量大于 512 字節(jié),則返回大于目標(biāo)容量的第一個(gè) 2 的指數(shù)冪。比如申請(qǐng)的 1000 字節(jié),那么返回的將是:2^10 = 1024。
PoolArena 提供了兩種方式進(jìn)行內(nèi)存分配:
①PoolSubpage 用于分配小于 8k 的內(nèi)存
tinySubpagePools:用于分配小于 512 字節(jié)的內(nèi)存,默認(rèn)長(zhǎng)度為 32,因?yàn)閮?nèi)存分配最小為 16,每次增加 16,直到 512,區(qū)間 [16,512) 一共有 32 個(gè)不同值。
smallSubpagePools:用于分配大于等于 512 字節(jié)的內(nèi)存,默認(rèn)長(zhǎng)度為 4。tinySubpagePools 和 smallSubpagePools 中的元素默認(rèn)都是 subpage。
②poolChunkList 用于分配大于 8k 的內(nèi)存
上面已經(jīng)解釋了 q 開頭的幾個(gè)變量用于保存大于 8k 的數(shù)據(jù)。
默認(rèn)先嘗試從 poolThreadCache 中分配內(nèi)存,PoolThreadCache 利用 ThreadLocal 的特性,消除了多線程競(jìng)爭(zhēng),提高內(nèi)存分配效率;
首次分配時(shí),poolThreadCache 中并沒有可用內(nèi)存進(jìn)行分配,當(dāng)上一次分配的內(nèi)存使用完并釋放時(shí),會(huì)將其加入到 poolThreadCache 中,提供該線程下次申請(qǐng)時(shí)使用。
如果是分配小內(nèi)存,則嘗試從 tinySubpagePools 或 smallSubpagePools 中分配內(nèi)存,如果沒有合適 subpage,則采用方法 allocateNormal 分配內(nèi)存。
如果分配一個(gè) page 以上的內(nèi)存,直接采用方法 allocateNormal() 分配內(nèi)存,allocateNormal() 則會(huì)將申請(qǐng)動(dòng)作交由 PoolChunkList 進(jìn)行。
- private synchronized void allocateNormal(PooledByteBuf
buf, int reqCapacity, int normCapacity) { - //如果在對(duì)應(yīng)的PoolChunkList能申請(qǐng)到內(nèi)存,則返回
- 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)) {
- ++allocationsNormal;
- return;
- }
- // Add a new chunk.
- PoolChunk
c = newChunk(pageSize, maxOrder, pageShifts, chunkSize); - long handle = c.allocate(normCapacity);
- ++allocationsNormal;
- assert handle > 0;
- c.initBuf(buf, handle, reqCapacity);
- qInit.add(c);
- }
首先將申請(qǐng)動(dòng)作按照 q050→q025→q000→qInit→q075 的順序依次交由各個(gè) PoolChunkList 進(jìn)行處理,如果在對(duì)應(yīng)的 PoolChunkList 中申請(qǐng)到了內(nèi)存,則直接返回。
如果申請(qǐng)不到,那么直接創(chuàng)建一個(gè)新的 PoolChunk,然后在該 PoolChunk 中申請(qǐng)目標(biāo)內(nèi)存,最后將該 PoolChunk 添加到 qInit 中。
上面說過 Chunk 是 Netty 向操作系統(tǒng)申請(qǐng)內(nèi)存塊的最大單位,每個(gè) Chunk 是 16M。
PoolChunk 內(nèi)部通過 memoryMap 數(shù)組維護(hù)了一顆完全平衡二叉樹作為管理底層內(nèi)存分布及回收的標(biāo)記位,所有的子節(jié)點(diǎn)管理的內(nèi)存也屬于其父節(jié)點(diǎn)。
關(guān)于 PoolChunk 內(nèi)部如何維護(hù)完全平衡二叉樹就不在這里展開,大家有興趣可以自行看源碼。
對(duì)于內(nèi)存的釋放,PoolArena 主要是分為兩種情況,即池化和非池化,如果是非池化,則會(huì)直接銷毀目標(biāo)內(nèi)存塊,如果是池化的,則會(huì)將其添加到當(dāng)前線程的緩存中。
如下是 free() 方法的源碼:
- public void free(PoolChunk
chunk, ByteBuffer nioBuffer, long handle, int normCapacity, - PoolThreadCache cache) {
- // 如果是非池化的,則直接銷毀目標(biāo)內(nèi)存塊,并且更新相關(guān)的數(shù)據(jù)
- if (chunk.unpooled) {
- int size = chunk.chunkSize();
- destroyChunk(chunk);
- activeBytesHuge.add(-size);
- deallocationsHuge.increment();
- } else {
- // 如果是池化的,首先判斷其是哪種類型的,即tiny,small或者normal,
- // 然后將其交由當(dāng)前線程的緩存進(jìn)行處理,如果添加成功,則直接返回
- SizeClass sizeClass = sizeClass(normCapacity);
- if (cache != null && cache.add(this, chunk, nioBuffer, handle,
- normCapacity, sizeClass)) {
- return;
- }
- // 如果當(dāng)前線程的緩存已滿,則將目標(biāo)內(nèi)存塊返還給公共內(nèi)存塊進(jìn)行處理
- freeChunk(chunk, handle, sizeClass, nioBuffer);
- }
- }
作者:rickiyang
編輯:陶家龍
出處:轉(zhuǎn)載自微信公眾號(hào) rickiyang
新聞名稱:昨天,我徹底搞懂了Netty內(nèi)存分配策略!
URL網(wǎng)址:http://www.5511xx.com/article/cdgeeoo.html


咨詢
建站咨詢
