Netty內(nèi)存管理機(jī)制

1、內(nèi)存管理介紹

內(nèi)存管理的目的是合理分配內(nèi)存,減少內(nèi)存碎片,及時回收資源,提高內(nèi)存的使用資源。

可以帶著以下問題進(jìn)行研究:

  • 內(nèi)存池管理算法是如何實現(xiàn)高效內(nèi)存分配釋放,減少內(nèi)存碎片?
  • 高負(fù)載下內(nèi)存池不斷申請/釋放,如何實現(xiàn)彈性伸縮?
  • 內(nèi)存池作為全局?jǐn)?shù)據(jù),在多線程環(huán)境下如何減少鎖競爭?

常見的一些算法有slab,buddy,jemalloc等經(jīng)典算法。

Netty中的內(nèi)存管理應(yīng)該是借鑒了FreeBSD內(nèi)存管理的思想——jemalloc。Netty內(nèi)存分配過程中總體遵循以下規(guī)則:

  • 優(yōu)先從緩存中分配
  • 如果緩存中沒有的話,從內(nèi)存池看看有沒有剩余可用的
  • 如果已申請的沒有的話,再真正申請內(nèi)存
  • 分段管理,每個內(nèi)存大小范圍使用不同的分配策略

2、分配算法

jemalloc依賴多個Arena來分配內(nèi)存,運(yùn)行中的應(yīng)用都有固定數(shù)量的多個Arena,默認(rèn)的數(shù)量與處理器的個數(shù)相關(guān)。系統(tǒng)中多個Arena的原因是由于各個線程進(jìn)行內(nèi)存分配時競爭不可避免,Netty允許使用者創(chuàng)建多個分配器來分離鎖,提高內(nèi)存分配效率。

內(nèi)存分配的調(diào)用堆??磧?nèi)存分配的主要過程:

  1. new一個ByteBuf,如果是direct則new:PooledUnsafeDirectByteBuf
  2. 從緩存中查找,沒有可用的緩存進(jìn)行下一步
  3. 從內(nèi)存池中查找可用的內(nèi)存,查找的方式如上所述(tiny、small、normal)
  4. 如果找不到則重新申請內(nèi)存,并將申請到的內(nèi)存放入內(nèi)存池
  5. 使用申請到的內(nèi)存初始化ByteBuf

線程首次分配/回收內(nèi)存時,首先會為其分配一個固定的Arena。線程選擇Arena時使用round-robin的方式,也就是順序輪流選取。

各個線程保存各種Arena和緩沖池信息,這樣可以減少競爭并提高訪問效率。

Arena將內(nèi)存分為很多Chunk進(jìn)行管理,Chunk內(nèi)存保存Page,以頁為單位申請。
申請內(nèi)存分配時,會將分配到的規(guī)格分為幾類:TINY,SMAILL,NORMAL和HUGE,分別對應(yīng)不同的范圍,處理過程也不相同。
![image.png](https://upload-images.jianshu.io/upload_images/6271376-d6a0844f340a893a.png

  1. 內(nèi)存分配的最小單位為16B。
  2. 小于512B的請求為Tiny,小于8KB(PageSize)的請求為Small,小于等于16MB(Chunk Size)的請求為Normal,大于16MB(Chun kSize)的請求為Huge。
  3. 小于512B的請求以16B為起點(diǎn)每次增加16B;大于等于512B的請求則每次加倍。

為了分配內(nèi)存塊保存連續(xù)和減少內(nèi)存碎片,因此Jemalloc使用Buddy內(nèi)存分配算法。

其實使用二叉樹進(jìn)行管理,樹中每個葉子節(jié)點(diǎn)表示一個Page,即樹高為12。具有相同父節(jié)點(diǎn)的葉子節(jié)點(diǎn)稱為buddy關(guān)系,buddy之間自底向上鏈接為二叉樹,直到根節(jié)點(diǎn)。

image.png

舉個例子:8KB、16KB、8KB為例分析分配過程(每個Page大小8KB):

  1. 8KB:需要一個Page,第11層滿足要求,故分配2048節(jié)點(diǎn)即Page0;
  2. 16KB:需要兩個Page,故需要在第10層進(jìn)行分配,而1024的子節(jié)點(diǎn)2048已分配,從左到右找到滿足要求的1025節(jié)點(diǎn),故分配節(jié)點(diǎn)1025即Page2和Page3;
  3. 8KB:需要一個Page,第11層滿足要求,2048已分配,從左到右找到2049節(jié)點(diǎn)即Page1進(jìn)行分配。

分配結(jié)束后,已分配連續(xù)的Page0-Page3,這樣的連續(xù)內(nèi)存塊,大大減少內(nèi)部碎片并提高內(nèi)存使用率

ByteBuf分類

Netty使用ByteBuf對象作為數(shù)據(jù)容器,進(jìn)行I/O讀寫操作,Netty的內(nèi)存管理也是圍繞著ByteBuf對象高效地分配和釋放

當(dāng)討論ByteBuf對象管理,主要從以下方面進(jìn)行分類:

Pooled 和 Unpooled

  • 池化內(nèi)存分配時基于預(yù)分配的一整塊大內(nèi)存,取其中的部分封裝成ByteBuf提供使用,用完后回收到內(nèi)存池中。
  • 非池化內(nèi)存每次分配時直接調(diào)用系統(tǒng) API 向操作系統(tǒng)申請ByteBuf需要的同樣大小內(nèi)存,用完后通過系統(tǒng)調(diào)用進(jìn)行釋放Pooled。

tips: Netty4默認(rèn)使用Pooled的方式,可通過參數(shù)-Dio.netty.allocator.type=unpooled或pooled進(jìn)行設(shè)置

Heap 和 Direct

  • Heap,指ByteBuf關(guān)聯(lián)的內(nèi)存JVM堆內(nèi)分配,分配的內(nèi)存受GC 管理
  • Direct,指ByteBuf關(guān)聯(lián)的內(nèi)存在JVM堆外分配,分配的內(nèi)存不受GC管理,需要通過系統(tǒng)調(diào)用實現(xiàn)申請和釋放,底層基于Java NIO的DirectByteBuffer對象

申請/釋放內(nèi)存

當(dāng)申請分配內(nèi)存,會首先將請求分配的內(nèi)存大小歸一化(向上取值),通過PoolArena#normalizeCapacity()方法,取最近的2的冪的值?,例如8000byte歸一化為8192byte( chunkSize/2^11 ),8193byte歸一化為16384byte(chunkSize/2^10)

處理內(nèi)存申請的算法在PoolChunk#allocateRun方法中,當(dāng)分配已歸一化處理后大小為chunkSize/2^d的內(nèi)存,即需要在depth = d的層級中找到第一塊空閑內(nèi)存,算法從根節(jié)點(diǎn)開始遍歷 (根節(jié)點(diǎn)depth = 0, id = 1),具體步驟如下:

  1. 步驟1 判斷是否當(dāng)前節(jié)點(diǎn)值memoryMap[id] > d,如果是,則無法從該chunk分配內(nèi)存,查找結(jié)束。
  2. 步驟2 判斷是否節(jié)點(diǎn)值memoryMap[id] == d,且depth_of_id == h
    。如果是,當(dāng)前節(jié)點(diǎn)是depth = d的空閑內(nèi)存,查找結(jié)束,更新當(dāng)前節(jié)點(diǎn)值為memoryMap[id] = max_order + 1,代表節(jié)點(diǎn)已使用,并遍歷當(dāng)前節(jié)點(diǎn)的所有祖先節(jié)點(diǎn),更新節(jié)點(diǎn)值為各自的左右子節(jié)點(diǎn)值的最小值;如果否,執(zhí)行步驟3
  3. 步驟3 判斷是否當(dāng)前節(jié)點(diǎn)值memoryMap[id] <= d,且depth_of_id < h。如果是,則空閑節(jié)點(diǎn)在當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)中,則先判斷左子節(jié)點(diǎn)memoryMap[2 * id] <=d(判斷左子節(jié)點(diǎn)是否可分配),如果成立,則當(dāng)前節(jié)點(diǎn)更新為左子節(jié)點(diǎn),否則更新為右子節(jié)點(diǎn),然后重復(fù)步驟2。

釋放內(nèi)存

釋放內(nèi)存時,根據(jù)申請內(nèi)存返回的id,將 memoryMap[id]更新為depth_of_id,同時設(shè)置id節(jié)點(diǎn)的祖先節(jié)點(diǎn)值為各自左右節(jié)點(diǎn)的最小值。

巨型對象內(nèi)存管理

對于申請分配大小超過chunkSize的巨型對象(huge),Netty采用的是非池化管理策略,在每次請求分配內(nèi)存時單獨(dú)創(chuàng)建特殊的非池化PoolChunk對象進(jìn)行管理,內(nèi)部memoryMap為null,當(dāng)對象內(nèi)存釋放時整個Chunk內(nèi)存釋放,相應(yīng)內(nèi)存申請邏輯在PoolArena#allocateHuge()方法中,釋放邏輯在PoolArena#destroyChunk()方法中。

小對象內(nèi)存管理

這些小對象直接分配一個page會造成浪費(fèi),在page中進(jìn)行平衡樹的標(biāo)記又額外消耗更多空間,因此Netty的實現(xiàn)是:先PoolChunk中申請空閑page,同一個page分為相同大小規(guī)格的小內(nèi)存進(jìn)行存儲。

image.png

彈性伸縮

PoolChunk管理

為了解決單個PoolChunk容量有限的問題,Netty將多個PoolChunk組成鏈表一起管理,然后用PoolChunkList對象持有鏈表的head

將所有PoolChunk組成一個鏈表的話,進(jìn)行遍歷查找管理效率較低,因此Netty設(shè)計了PoolArena對象(arena中文是舞臺、場所),實現(xiàn)對多個PoolChunkList、PoolSubpage的管理,線程安全控制、對外提供內(nèi)存分配、釋放的服務(wù)。

PoolSubpage管理

PoolArena內(nèi)部持有2個PoolSubpage數(shù)組,分別存儲tiny和small規(guī)格類型的PoolSubpage

并發(fā)設(shè)計

為了減少線程間的競爭,Netty會提前創(chuàng)建多個PoolArena(默認(rèn)生成數(shù)量 = 2 * CPU核心數(shù)),當(dāng)線程首次請求池化內(nèi)存分配,會找被最少線程持有的PoolArena,并保存線程局部變量PoolThreadCache中,實現(xiàn)線程與PoolArena的關(guān)聯(lián)綁定(PoolThreadLocalCache#initialValue()方法)。

Netty設(shè)計了ThreadLocal的更高性能替代類:FastThreadLocal,需要配套繼承Thread的類FastThreadLocalThread一起使用,基本原理是將原來Thead的基于ThreadLocalMap存儲局部變量,擴(kuò)展為能更快速訪問的數(shù)組進(jìn)行存儲(Object[] indexedVariables),每個FastThreadLocal內(nèi)部維護(hù)了一個全局原子自增的int類型的數(shù)組index。

Netty還設(shè)計了緩存機(jī)制提升并發(fā)性能:當(dāng)請求對象內(nèi)存釋放,PoolArena并沒有馬上釋放,而是先嘗試將該內(nèi)存關(guān)聯(lián)的PoolChunk和chunk中的偏移位置(handler變量)等信息存入PoolThreadLocalCache中的固定大小緩存隊列中(如果緩存隊列滿了則馬上釋放內(nèi)存);當(dāng)請求內(nèi)存分配,PoolArena會優(yōu)先訪問PoolThreadLocalCache的緩存隊列中是否有緩存內(nèi)存可用,如果有,則直接分配,提高分配效率。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 才畢業(yè)的大學(xué)生,或者是跨行到未涉及領(lǐng)域的職場人,總喜歡在面試的時候吹噓自己的才華,把自己說的無所不能,以為只要蒙混...
    不愛牛奶的旺仔閱讀 1,157評論 1 2
  • 昨天去試聽了一節(jié)美術(shù)課,要求畫媽媽,爸爸和孩子,但潤潤只畫了媽媽和他自己。老師說,他獲得了一點(diǎn)特別好,畫孩子的眼眉...
    慢蝸牛Erica閱讀 422評論 0 1
  • (轉(zhuǎn)自https://github.com/oa414/objc-zen-book-cn 禪與 Objective...
    小狄愛玩雪閱讀 292評論 0 0
  • 今天開了年會,聚餐,多天練歌沒有白練,雖然唱歌時覺得心臟要跳了出來,但看錄像很滿意。
    靜冥兒閱讀 121評論 0 1

友情鏈接更多精彩內(nèi)容