本篇內容主要講解“Netty內存管理怎么理解”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Netty內存管理怎么理解”吧!
中江網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁設計、網(wǎng)站建設、微信開發(fā)、APP開發(fā)、響應式網(wǎng)站等網(wǎng)站項目制作,到程序開發(fā),運營維護。成都創(chuàng)新互聯(lián)公司2013年至今到現(xiàn)在10年的時間,我們擁有了豐富的建站經(jīng)驗和運維經(jīng)驗,來保證我們的工作的順利進行。專注于網(wǎng)站建設就選成都創(chuàng)新互聯(lián)公司。
正是Netty的易用性和高性能成就了Netty,讓其能夠如此流行。
而作為一款通信框架,首當其沖的便是對IO性能的高要求。
不少讀者都知道Netty底層通過使用Direct Memory,減少了內核態(tài)與用戶態(tài)之間的內存拷貝,加快了IO速率。但是頻繁的向系統(tǒng)申請Direct Memory,并在使用完成后釋放本身就是一件影響性能的事情。為此,Netty內部實現(xiàn)了一套自己的內存管理機制,在申請時,Netty會一次性向操作系統(tǒng)申請較大的一塊內存,然后再將大內存進行管理,按需拆分成小塊分配。而釋放時,Netty并不著急直接釋放內存,而是將內存回收以待下次使用。
這套內存管理機制不僅可以管理Directory Memory,同樣可以管理Heap Memory。
這里,我想向讀者們強調一點,ByteBuf和內存其實是兩個概念,要區(qū)分理解。
ByteBuf是一個對象,需要給他分配一塊內存,它才能正常工作。
而內存可以通俗的理解成我們操作系統(tǒng)的內存,雖然申請到的內存也是需要依賴載體存儲的:堆內存時,通過byte[], 而Direct內存,則是Nio的ByteBuffer(因此Java使用Direct Memory的能力是JDK中Nio包提供的)。
為什么要強調這兩個概念,是因為Netty的內存池(或者稱內存管理機制)涉及的是針對內存的分配和回收,而Netty的ByteBuf的回收則是另一種叫做對象池的技術(通過Recycler實現(xiàn))。
雖然這兩者總是伴隨著一起使用,但這二者是獨立的兩套機制??赡艽嬖谥炒蝿?chuàng)建ByteBuf時,ByteBuf是回收使用的,而內存卻是新向操作系統(tǒng)申請的。也可能存在某次創(chuàng)建ByteBuf時,ByteBuf是新創(chuàng)建的,而內存卻是回收使用的。
因為對于一次創(chuàng)建過程而言,可以分成三個步驟:
獲取ByteBuf實例(可能新建,也可能是之間緩存的)
向Netty內存管理機制申請內存(可能新向操作系統(tǒng)申請,也可能是之前回收的)
將申請到的內存分配給ByteBuf使用
本文只關注內存的管理機制,因此不會過多的對對象回收機制做解釋。
Netty中與內存管理相關的類有很多??蚣軆炔刻峁┝?code>PoolArena,PoolChunkList
,PoolChunk
,PoolSubpage
等用來管理一塊或一組內存。
而對外,提供了ByteBufAllocator
供用戶進行操作。
接下來,我們會先對這幾個類做一定程度的介紹,在通過ByteBufAllocator
了解內存分配和回收的流程。
為了篇幅和可讀性考慮,本文不會涉及到大量很詳細的代碼說明,而主要是通過圖輔之必要的代碼進行介紹。
針對代碼的注解,可以見我GitHub上的netty項目。
上文已經(jīng)介紹了,為了減少頻繁的向操作系統(tǒng)申請內存的情況,Netty會一次性申請一塊較大的內存。而后對這塊內存進行管理,每次按需將其中的一部分分配給內存使用者(即ByteBuf)。這里的內存就是PoolChunk
,其大小由ChunkSize決定(默認為16M,即一次向OS申請16M的內存)。
PoolChunk所能管理的最小內存叫做Page,大小由PageSize(默認為8K),即一次向PoolChunk申請的內存都要以Page為單位(一個或多個Page)。
當需要由PoolChunk分配內存時,PoolChunk會查看通過內部記錄的信息找出滿足此次內存分配的Page的位置,分配給使用者。
我們已經(jīng)知道PoolChunk內部會以Page為單位組織內存,同樣以Page為單位分配內存。
那么PoolChunk要如何管理才能兼顧分配效率(指盡可能快的找出可分配的內存且保證此次分配的內存是連續(xù)的)和使用效率(盡可能少的避免內存浪費,做到物盡其用)的?
Netty采用了Jemalloc的想法。
首先PoolChunk通過一個完全二叉樹來組織內部的內存。以默認的ChunkSize為16M, PageSize為8K為例,一個PoolChunk可以劃分成2048個Page。將這2048個Page看作是葉子節(jié)點的寬度,可以得到一棵深度為11的樹(2^11=2048)。
我們讓每個葉子節(jié)點管理一個Page,那么其父節(jié)點管理的內存即為兩個Page(其父節(jié)點有左右兩個葉子節(jié)點),以此類推,樹的根節(jié)點管理了這個PoolChunk所有的Page(因為所有的葉子結點都是其子節(jié)點),而樹中某個節(jié)點所管理的內存大小即是以該節(jié)點作為根的子樹所包含的葉子節(jié)點管理的全部Page。
這樣做的好處就是當你需要內存時,很快可以找到從何處分配內存(你只需要從上往下找到所管理的內存為你需要的內存的節(jié)點,然后將該節(jié)點所管理的內存分配出去即可),并且所分配的內存還是連續(xù)的(只要保證相鄰葉子節(jié)點對應的Page是連續(xù)的即可)。
上圖中編號為512的節(jié)點管理了4個Page,為Page0, Page1, Page2, Page3(因為其下面有四個葉子節(jié)點2048,2049,2050, 2051)。
而編號為1024的節(jié)點管理了2個Page,為Page0和Page1(其對應的葉子節(jié)點為Page0和Page1)。
當需要分配32K的內存時,只需要將編號512的節(jié)點分配出去即可(512分配出去后會默認其下所有子節(jié)點都不能分配)。而當需要分配16K的內存時,只需要將編號1024的節(jié)點分配出去即可(一旦節(jié)點1024被分配,下面的2048和2049都不允許再被分配)。
了解了PoolChunk內部的內存管理機制后,讀者可能會產(chǎn)生幾個問題:
PoolChunk內部如何標記某個節(jié)點已經(jīng)被分配?
當某個節(jié)點被分配后,其父節(jié)點所能分配的內存如何更新?即一旦節(jié)點2048被分配后,當你再需要16K的內存時,就不能從節(jié)點1024分配,因為現(xiàn)在節(jié)點1024可用的內存僅有8K。
為了解決以上這兩點問題,PoolChunk都是內部維護了的byte[] memeoryMap和byte[] depthMap兩個變量。
這兩個數(shù)組的長度是相同的,長度等于樹的節(jié)點數(shù)+1。因為它們把根節(jié)點放在了1的位置上。而數(shù)組中父節(jié)點與子節(jié)點的位置關系為:
假設parnet的下標為i,則子節(jié)點的下標為2i和2i+1
用數(shù)組表示一顆二叉樹,你們是不是想到了堆這個數(shù)據(jù)結構。
已經(jīng)知道了兩個數(shù)組都是表示二叉樹,且數(shù)組中的每個元素可以看成二叉樹的節(jié)點。那么再來看看元素的值分別代碼什么意思。
對于depthMap而言,該值就代表該節(jié)點所處的樹的層數(shù)。例如:depthMap[1] == 1,因為它是根節(jié)點,而depthMap[2] = depthMap[3] = 2,表示這兩個節(jié)點均在第二層。由于樹一旦確定后,結構就不在發(fā)生改變,因此depthMap在初始化后,各元素的值也就不發(fā)生變化了。
而對于memoryMap而言,其值表示該節(jié)點下可用于完整內存分配的最小層數(shù)(或者說最靠近根節(jié)點的層數(shù))。
這話理解起來可能有點別扭,還是用上文的例子為例 。
首先在內存都未分配的情況下,每個節(jié)點所能分配的內存大小就是該層最初始的狀態(tài)(即memoryMap的初始狀態(tài)和depthMap的一致的)。而一旦其有個子節(jié)點被分配出后去,父節(jié)點所能分配的完整內存(完整內存是指該節(jié)點所管理的連續(xù)的內存塊,而非該節(jié)點剩余的內存大小)就減小了(內存的分配和回收會修改關聯(lián)的mermoryMap中相關節(jié)點的值)。
譬如,節(jié)點2048被分配后,那么對于節(jié)點1024來說,能完整分配的內存(原先為16K)就已經(jīng)和編號2049節(jié)點(其右子節(jié)點)相同(減為了8K),換句話說節(jié)點1024的能力已經(jīng)退化到了2049節(jié)點所在的層節(jié)點所擁有的能力。
這一退化可能會影響所有的父節(jié)點。
而此時,512節(jié)點能分配的完整內存是16K,而非24K(因為內存分配都是按2的冪進行分配,盡管一個消費者真實需要的內存可能是21K,但是Netty的內存管理機制會直接分配32K的內存)。
但是這并不是說節(jié)點512管理的另一個8K內存就浪費了,8K內存還可以用來在申請內存為8K的時候分配。
用圖片演示PoolChunk內存分配的過程。其中value表示該節(jié)點在memoeryMap的值,而depth表示該節(jié)點在depthMap的值。
第一次內存分配,申請者實際需要6K的內存:
這次分配造成的后果是其所有父節(jié)點的memoryMap的值都往下加了一層。
之后申請者需要申請12K的內存:
由于節(jié)點1024已經(jīng)無法分配所需的內存,而節(jié)點512還能夠分配,因此節(jié)點512讓其右節(jié)點再嘗試。
上述介紹的是內存分配的過程,而內存回收的過程就是上述過程的逆過程——回收后將對應節(jié)點的memoryMap的值修改回去。這里不過多介紹。
PoolChunkList內部有一個PoolChunk組成的鏈表。通常一個PoolChunkList中的所有PoolChunk使用率(已分配內存/ChunkSize)都在相同的范圍內。
每個PoolChunkList有自己的最小使用率或者最大使用率的范圍,PoolChunkList與PoolChunkList之間又會形成鏈表,并且使用率范圍小的PoolChunkList會在鏈表中更加靠前。
而隨著PoolChunk的內存分配和使用,其使用率發(fā)生變化后,PoolChunk會在PoolChunkList的鏈表中,前后調整,移動到合適范圍的PoolChunkList內。
這樣做的好處是,使用率的小的PoolChunk可以先被用于內存分配,從而維持PoolChunk的利用率都在一個較高的水平,避免內存浪費。
PoolChunk管理的最小內存是一個Page(默認8K),而當我們需要的內存比較小時,直接分配一個Page無疑會造成內存浪費。
PoolSubPage就是用來管理這類細小內存的管理者。
小內存是指小于一個Page的內存,可以分為Tiny和Smalll,Tiny是小于512B的內存,而Small則是512到4096B的內存。如果內存塊大于等于一個Page,稱之為Normal,而大于一個Chunk的內存塊稱之為Huge。
而Tiny和Small內部又會按具體內存的大小進行細分。
對Tiny而言,會分成16,32,48...496(以16的倍數(shù)遞增),共31種情況。
對Small而言,會分成512,1024,2048,4096四種情況。
PoolSubpage會先向PoolChunk申請一個Page的內存,然后將這個page按規(guī)格劃分成相等的若干個內存塊(一個PoolSubpage僅會管理一種規(guī)格的內存塊,例如僅管理16B,就將一個Page的內存分成512個16B大小的內存塊)。
每個PoolSubpage僅會選一種規(guī)格的內存管理,因此處理相同規(guī)格的PoolSubpage往往是通過鏈表的方式組織在一起,不同的規(guī)格則分開存放在不同的地方。
并且總是管理一個規(guī)格的特性,讓PoolSubpage在內存管理時不需要使用PoolChunk的完全二叉樹方式來管理內存(例如,管理16B的PoolSubpage只需要考慮分配16B的內存,當申請32B的內存時,必須交給管理32B的內存來處理),僅用 long[] bitmap (可以看成是位數(shù)組)來記錄所管理的內存塊中哪些已經(jīng)被分配(第幾位就表示第幾個內存塊)。
實現(xiàn)方式要簡單很多。
PoolArena是內存管理的統(tǒng)籌者。
它內部有一個PoolChunkList組成的鏈表(上文已經(jīng)介紹過了,鏈表是按PoolChunkList所管理的使用率劃分)。
此外,它還有兩個PoolSubpage的數(shù)組,PoolSubpage[] tinySubpagePools 和 PoolSubpage[] smallSubpagePools。
默認情況下,tinySubpagePools的長度為31,即存放16,32,48...496這31種規(guī)格的PoolSubpage(不同規(guī)格的PoolSubpage存放在對應的數(shù)組下標中,相同規(guī)格的PoolSubpage在同一個數(shù)組下標中形成鏈表)。
同理,默認情況下,smallSubpagePools的長度為4,存放512,1024,2048,4096這四種規(guī)格的PoolSubpage。
PoolArena會根據(jù)所申請的內存大小決定是找PoolChunk還是找對應規(guī)格的PoolSubpage來分配。
值得注意的是,PoolArena在分配內存時,是會存在競爭的,因此在關鍵的地方,PoolArena會通過sychronize來保證線程的安全。
Netty對這種競爭做了一定程度的優(yōu)化,它會分配多個PoolArena,讓線程盡量使用不同的PoolArena,減少出現(xiàn)競爭的情況。
PoolArena免不了產(chǎn)生競爭,Netty除了創(chuàng)建多個PoolArena減少競爭外,還讓線程在釋放內存時緩存已經(jīng)申請過的內存,而不立即歸還給PoolArena。
緩存的內存被存放在PoolThreadCache內,它是一個線程本地變量,因此是線程安全的,對它的訪問也不需要上鎖。
PoolThreadCache內部是由MemeoryRegionCache的緩存池(數(shù)組),同樣按等級可以分為Tiny,Small和Normal(并不緩存Huge,因為Huge效益不高)。
其中Tiny和Small這兩個等級下的劃分方式和PoolSubpage的劃分方式相同,而Normal因為組合太多,會有一個參數(shù)控制緩存哪些規(guī)格(例如,一個Page, 兩個Page和四個Page等...),不在Normal緩存規(guī)格內的內存塊將不會被緩存,直接還給PoolArena。
再看MemoryRegionCache, 它內部是一個隊列,同一隊列內的所有節(jié)點可以看成是該線程使用過的同一規(guī)格的內存塊。同時,它還有個size屬性控制隊列過長(隊列滿后,將不在緩存該規(guī)格的內存塊,而是直接還給PoolArena)。
當線程需要內存時,會先從自己的PoolThreadCache中找對應等級的緩存池(對應的數(shù)組)。然后再從數(shù)組中找出對應規(guī)格的MemoryRegionCache。最后從其隊列中取出內存塊進行分配。
在了解了上述這么多概念后,通過一張圖給讀者加深下印象。
上圖僅詳細畫了針對Heap Memory的部分,Directory Memory也是類似的。
最后在由PooledByteBufAllocator作為入口,重頭梳理一遍內存申請的過程:
PooledByteBufAllocator.newHeapBuffer()開始申請內存
獲取線程本地的變量PoolThreadCache以及和線程綁定的PoolArena
通過PoolArena分配內存,先獲取ByteBuf對象(可能是對象池回收的也可能是創(chuàng)建的),在開始內存分配
分配前先判斷此次內存的等級,嘗試從PoolThreadCache的找相同規(guī)格的緩存內存塊使用,沒有則從PoolArena中分配內存
對于Normal等級內存而言,從PoolChunkList的鏈表中找合適的PoolChunk來分配內存,如果沒有則先像OS申請一個PoolChunk,在由PoolChunk分配相應的Page
對于Tiny和Small等級的內存而言,從對應的PoolSubpage緩存池中找內存分配,如果沒有PoolSubpage,線會到第5步
到此,相信大家對“Netty內存管理怎么理解”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!