netty源码解读六(内存池相关)
申請內存到底是申請什么?
申請的地址,包括,某塊內存地址+本次使用偏移量+更具體的偏移量(如果有必要);業務拿到該地址后就可以將數據存放到該處,使用結束后再歸還;
PooledByteBufAllocator#newDirectBuffer方法中主要調用ByteBuf buf = directArena.allocate(xxx),而該方法中主要調用allocate(xxx)方法;
allocateNormal方法新建一個chunk
// Add a new chunk.
PoolChunk c = newChunk(pageSize, nPSizes, pageShifts, chunkSize);
boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
assert success;
qInit.add?;
newChunk
1)調用了PoolChunk類的構造方法,而該構造方法中又調用了ByteBuffer.allocateDirect方法申請了16mb內存,即返回了jdk的ByteBuffer對象,賦值給了memory屬性,因此poolChunk實例管理著16mb內存;盡管物理上PoolChunk是一個16M的內存空間,但邏輯上會按照樹狀結構來維護:關于這顆樹有幾點要說明:
? 1.1)PoolChunk會按照層數將16M的內存等分,第0層1個16M,第一層2個8M,第三層4個4M,依次類推直到第十一層,分成了2048個8K;
? 1.2)葉子節點的大小為8K;
? 1.3)為了快速找到節點層數,大小等關系,PoolChunk里維護了兩個數組,depthMap維護了節點的深度值,初始化后不能改變;memoryMap的值和depthMap完全相同,只是memoryMap會改變,表示該節點是否可用;深度值從0開始,最底層的深度值為11;
1.4)PoolChunk中實際上并沒有維護存儲節點大小的二叉樹,而是維護了如上圖存儲各節點深度值的二叉樹,分配內存的時候,總是根據需要的內存大小定位到深度值,然后在memoryMap中尋找合適的節點;
Netty 4.1.61與4.1.45在內存池方面有一些區別,4.1.61中雖然利用的是LongLongHashMap和LongPriorityQueue,沒找到memoryMap和deptMap,但是其實本質還是利用滿二叉樹實現的;memoryMap表示樹上每個節點的分配能力值,數組長度為最多能分配的SubPage數目的兩倍;滿二叉樹如何用數組表示呢?若父節點的索引為i,則左子節點的索引為2i,右子節點的索引為2i+1;數組的每個元素表示當前i下標的節點的可分配內存能力值,同一深度的樹節點可分配能力值一樣。如memoryMap[1]=0,表示根節點可分配內存能力值為0,該值越小,表示可分配能力越大,即根節點可分配16mb,memoryMap[2]=1,表示深度為1的節點可分配內存能力值為1,表示可分配8mb;memoryMap[0]空著不用;最終memoryMap數組被初始化為[0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3…];當 memoryMap對應索引的樹節點其管理的內存被占用后,該索引位置的值會發生改變;
deptMap的值表示當前索引的樹節點所在樹的深度是多少,初始化后,deptMap就不會發生改變;
PoolChunk#allocate
判斷請求是tiny,small類型的還是[8kb,16mb]的申請;
(4kb,16mb]的申請
1)先對申請內存的大小進行規格化得到一個2的冪大小的規格,接著計算 (4kb,16mb]的內存申請需要去滿二叉樹上哪一深度才能滿足要求,求出深度值d;
2)再根據深度d定位到具體那一層的哪個id空閑,并將其占用,設置已使用標記,即設置被占用的節點的深度能力值為12,表示該節點已經被全部使用,無法再繼續分配了;
3)最后修改所選id的父節點的分配能力值+1,是否加1這個要看父節點被影響的程度是否超過50%,如果影響部分小于50%,則只在第一次加1,若影響部分超過50%,則每影響一次就加1,之所以是50%,是因為[8kb,16mb]之間的分配,會默認按照2的n次冪的內存大小給申請者,比如16mb,被分配出去2mb和4mb最后結果是一樣的,即16mb所在節點剩下只能分配8mb了,所以分配能力值需要加1;另外需要修改memoryMap數組中被占用的相應位置值為12;12代表該點已無法再分配了,因為深度為11時,已經是最小分配單位了;
4)組裝handle,代表是申請[8kb,16mb]規格,低32位代表二叉樹上id;
比如一個線程先申請3M的內存,接著申請2M,過程如下:
a)確定內存大小,為了便于管理,對于>=8K的內存,Netty會默認返回2的n次冪的內存大小給申請者,所以Netty會申請4M的內存給調用者;
b)計算深度值:log2(16M) - log2(4M) = 24 - 22 = 2(統一變為kb進行計算);
c)根據深度值去memoryMap中查找合適的節點,并循環更新上層節點的值,新的值為左右子節點中較小的值(因為規定較小的值代表更大的分配能力);
d)申請2M的內存,重復b,c步;步驟如下圖所示,找到合適的節點后,將該節點的值更新為12;
tiny,small類型的申請[16b,4kb]
同樣也需要進行規格化,如申請10b,變為申請16b;16b,32b,48b…496b為tiny類型(每次遞增16b),512b,1024b,2kb,4kb為small類型(每次兩倍遞增);均小于8kb(一頁),所以需要去滿二叉樹上的葉子節點中找一個可用的葉子節點即創建SubPage實例,并且將該實例放入PoolSubPage數組中對應的下標處;滿二叉樹的葉子節點從2048到4095剛好對應PoolSubPage數組從0到2047,目的是存放PoolChunK創建出來的SubPage;
1) 找到head,去tinySubpagePools和smallSubpagePools數組中找得到head節點,如下圖所示;
2)計算當前葉子節點的管理內存在整個內存的偏移位置,以深度11作為入參,找分配能力為8kb的節點id號;計算該節點的runOffset,等于shift乘以runLength,假設節點id為2049,則得到2049的shift為1,runLength為8kb,所以得到runOffset為1乘以8kb得到8kb;假設節點id為2048,則得到2048的shift為0,runLength為8kb,所以得到runOffset為0乘以8kb得到0;2050的runOffset為16kb,2051的runOffset為24kb;(runOffset相當于起點地址)
3)創建PoolSubPage類型對象,用于管理PoolChunk創建出來的SubPage即一頁;首先調用其構造函數,6個非常重要的入參,依次是head節點,PoolChunk實例,當前SubPage實例對應葉子節點的id號,當前葉子節點的管理內存在整個內存的偏移位置,一頁大小(8kb),申請的大小(調整后的);構造方法中還會初始化位圖,將一頁按最小16b進行切割,即8kb/16b=512,每一位為1表示被占用,為0表示沒有被占用,512/64=8,即用8個long型的變量即可表示連續的512位,此時用數組可以表示long bitMap = new long[8];
4)在構造方法中,會初始化位圖bitMap,若申請規格為32b,則一頁8kb/32b為256位,需要4個long型,則初始化bitMap數組的前四位為0;若申請規格為48b,則一頁8kb/48b為170,需要3個long型,則初始化bitMap數組的前三位為0;
5)subPage.allocate方法中正式申請small,tiny規格內存
a)先從bitMap中查找一個可用的bit(即找到第一個為0的位),返回改bit的索引值;bitMap[0]代表0-63的索引,bitMap[1]代表64到127的索引…
b)再將該bit位置1;代表該快內存被占用了;
c)組裝handle值,該值最高位為1用于區分[8kb,16mb]規格的申請,最高位為1代表是申請small,tiny規格,高32位代表申請的內存在bitMap上索引值(每次只占一個索引,不可能占多個索引),而低32位代表二叉樹上占用節點的id;這里之所以要弄一個最高位為1,就是為了快速區分;
具體分配策略,參考如下文章
https://www.jianshu.com/p/aa2bb182466e
initBuf方法
根據上述兩種情況返回的handle初始化buf;
若申請的是[8kb,16mb],則相對簡單;
若申請的是small,tiny類型的內存,則需要根據bitMap已使用的索引值計算subPage的內部使用偏移量,公式是索引值乘以每位代表的大小,計算完之后,subPage的內部使用偏移量加subPage的runOffset就得到subPage在整個PoolChunk實例上的偏移量。
申請內存代碼流程(以堆外內存為例)
2021年11月份分析的:
此次分析比9月份要更為完整清晰。從AbstractByteBuf#readBytes(int length)方法開始,其中會執行AbstractByteBufAllocator#buffer方法,其中又會執行directBuffer方法,其中又會執行PooledByteBufAllocator#newDirectBuffer方法;其中會執行如下幾個方法:
1)threadCache.get()
拿到PoolThreadCache實例cache;
2)cache.directArena
拿到PoolArena類型的屬性directArena;
3)directArena.allocate方法
3.1)newByteBuf方法
到ByteBuf 對象池內 獲取一個空閑 ByteBuf 對象,并且重置ByteBuf 對象的 字段 等信息。
3.2)allocate方法;
3.2.1)normalizeCapacity(reqCapacity);
將reqCapacity轉換為符合規格的大小normCapacity;
3.2.2)isTinyOrSmall(normCapacity)方法,判斷申請規格是tiny或small
3.2.2.1)cache.allocateTiny若申請的是tiny規格,則從tiny緩存中拿;
3.2.2.2)cache.allocateSmall
若申請的是small規格,則從small緩存中拿;
3.2.2.3)PoolSubpage head = table[tableIdx];
從tinySubpagePools或smallSubpagePools數組中拿;
3.2.2.4)allocateNormal
3.2.2.4.1)PoolChunkList#allocate
嘗試從list集合中拿poolChunk,有的話則遍歷poolChunkList,執行PoolChunk#allocate方法,申請成功后,再次判斷當前poolChunk的使用率,將其轉至合適的poolChunkList中;
3.2.2.4.2)newChunk
拿到PoolChunk實例c;
3.2.2.4.3)c.allocate從新創建的PoolChunk內 分配內存
3.2.2.4.3.1)allocateRun
申請規格不小于一頁8kb,調用此方法;
a)求申請規格的深度值
深度值=log2(16mb)-log2(normCapacity);假如申請的是3mb,規格化后為4mb,所以4mb在滿二叉樹的深度值為log2(16mb)-log2(4mb)=24-22=2;
b)allocateNode(d)方法查找合適的節點
c)更新當前poolChunk的空閑大小freeBytes
直接根據當前分配的節點的id計算內存大小,再減掉,freeBytes初始值為16mb;之所以要計算剩余空閑內存大小,是因為在poolChunkList中會根據poolChunk使用率將其分配到不同的poolChunkList中;
3.2.2.4.3.2)allocateSubpage
申請規格小于一頁8kb,調用此方法;
3.2.2.4.3.3)initBuf
3.2.2.4.4)PoolChunkList#add
將新申請的poolChunk加入進合適的list中;
3.2.3)normCapacity <= chunkSize
說明normCapacity雖然大于maxSmallSize 4096,但是 小于 16mb;
3.2.3.1)allocateNormal方法
該方法和3.2.2.4)是同一個方法;
3.2.4)allocateHuge
說明 normCapacity 非常大,是大于16mb的!這個規格稱為 huge,需要走特殊的分配邏輯
4)toLeakAwareBuffer方法
資源泄露相關的處理邏輯;
以上是流程總覽;
allocateNode(d)方法的細節:
首先從根節點開始,每一層的最左節點分配能力值與目標深度值比較,若分配能力值小于目標深度值,則繼續看下一層的最左節點,若不小于,則當前深度是要找到深度,接著再找這一層的具體哪個id,比較當前最左節點的分配能力值是否大于目標深度值,若大于則表示該節點不夠分配,看其右邊節點;
該方法有一個while循環,while (val < d || (id & initial) == 0),val表示節點分配能力值,d代表深度值,(id & initial) == 0表示當前id是不在深度為d的層;
所謂合適的節點,指分配能力值合適且其下沒有子節點更合適,分配能力值合適比如申請1mb不能一下子給8mb,而分配能力值應該恰好為1mb;而其下沒有子節點更合適意味著當發現某個父節點分配能力為1mb,但其實父節點原本分配能力值為2mb,但其左子節點已被完全分配出去,而右子節點才是真正分配1mb的節點,所以此時該右子節點才是要找的;
該方法的邏輯是先判斷根節點是否夠分配,若不夠分配,則直接返回;若夠分配,則再檢查下一層的最左節點是否是更為合適的一個節點,此時有三種情況:
1)第一種情況是會一直找到深度d所在的層且最左節點就是合適節點;如需要找一個8kb,最終找到了深度為11的最左節點為合適節點;(val < d且其左子節點的val也小于d)
2)第二種情況是在小于深度d的層的節點,發現該節點已被分配出去了部分,此時需要找到其哪一個子節點可用;如需要找一個8kb,發現16kb已被分出去部分,此時還需要進一步找到可分配的子節點進行判斷;(val不小于 d但(id & initial)為0)
3)第三種情況是在小于深度d的層的節點,并沒有發現該節點已被分配出去了,實際上該節點已被分配出去了部分,只是沒有進一步影響到分配能力值,所以此時在判斷其子節點時,突然發現左子節點已不夠分配了,則此時跳到右子節點;如需要找一個8kb,發現32kb的左子節點不夠分配了,則找其右子節點進一步判斷;(val < d但其左子節點的val不小于d)
當找到所需節點后,最后循環更新父節點的分配能力值,去子節點中較小值,因為較小值代表著更大的分配能力;
handle = allocateSubpage(normCapacity)方法細節
申請的內存屬于tiny類型,則會將葉節點按照16byte、32byte、48byte…、496byte中的一種進行劃分。比如申請10byte的內存,則會按照16byte均等地劃分8K節點并返回16byte給調用者。
申請的內存屬于small類型,則會按照512byte、1024byte、2048byte和4096byte中的一種劃分。
申請小于8kb的內存,會進入此方法,此時會在poolChunk的葉子節點分配,PoolChunk會將葉子節點進行劃分,劃分的方式與申請的內存大小有關,對應tiny和small類型的內存,并不是按照2的n次冪進行申請,而是按照上述若干固定的大小進行分配。比如申請9byte,實際會申請16byte;申請40byte,實際會申請48byte;最后則會按照16byte和48byte均等地劃分8K節點并返回給調用者。
在申請normal類型的內存時,使用了memoryMap記錄節點的層數位置等信息;而在申請tiny或small時,均分的page使用了一個bitMap記錄分配的位置;比如申請10byte,則會促使一個葉子節點按照16byte進行劃分,總共劃分了 8K / 16byte = 512個,則bitMap = new long[512 / Long.SIZE] = new long[8];
1)首先執行int id = allocateNode(d)方法找到合適的葉子節點;
剩下部分和下邊分析的一致;
內存池回收
PooledByteBuf#deallocate方法;
1)設置handle為-1,memory為null(之前是保存jdk的16mb的byteBuffer),PoolChunk實例為null;
2)chunk.arena.free方法
釋放邏輯首選方案是將內存緩存到cache,以便 cache歸屬線程,后備之需;如果cache滿了,裝不了這么多內存了,就將內存歸還到chunk。
回收邏輯是根據handle,判斷歸還的是tiny,small類型的內存還是normal類型的;如果是tiny,small類型的內存則需要還原bitmap上的占用位為0,記錄當前歸還位置,下次申請可以直接定位到這里;如果歸還的是normal類型,則更新poolChunk中剩余空閑內存大小,更新節點分配能力值為深度值,恢復父節點的能力值;
3)recycle()方法
PooledByteBuf 對象 歸還到 “對象池”
資源泄漏監控
在newDirectBuffer方法的最后會執行toLeakAwareBuffer方法,該方法用于對byteBuf進行包裝;有幾種檢測級別, 默認是simple級別,即進行抽樣檢測,其他三種分別是disable,advanced,paranoid。advanced也是抽樣檢測但追蹤更詳細,paranoid是對所有byteBuf都檢測并且詳細追蹤,當為disable時,不對byteBuf進行包裝;ResourceLeakDetector類用于檢測資源,當實例化該類時,傳入什么類型的T,就監測什么類型,這里傳入的是ByteBuf.class;
toLeakAwareBuffer方法步驟如下:
1)如果是simple檢測級別
1.1)先執行ResourceLeakDetector#track方法
1.1.1)若檢測級別為disabled則返回null;
1.1.2)若檢測級別是simple則進行采樣,隨機數采樣,默認是128個byteBuf會追蹤一個,若采樣失敗,則返回null,否則執行以下兩步
1.1.2.1)reportLeak方法;
1.1.2.2)此時會調用DefaultResourceLeak的構造方法返回實例,DefaultResourceLeak繼承了WeakReference,構造方法中關聯了bytebuf,ReferenceQueue實例,并且將當前DefaultResourceLeak實例加入到了set集合中;
1.1.3)若檢測級別是advanced或paranoid,則直接執行1.1.2.1)和1.1.2.2)步驟;
1.2)若1.1返回值不為null,則返回一個SimpleLeakAwareByteBuf實例,封裝了當前byteBuf和1.1返回的DefaultResourceLeak實例;若1.1返回null,則此時直接返回byteBuf;
2)如果是advanced或paranoid檢測級別
則步驟和simple一樣,就是返回的是AdvancedLeakAwareByteBuf實例,封裝也一樣;若1.1返回null,則此時直接返回byteBuf;
當釋放資源時,會調用SimpleLeakAwareByteBuf#release方法
1)若super.release方法返回true
則執行closeLeak方法;該方法 中會執行leak.close方法,會將當前DefaultResourceLeak實例從set集合中移除,以及斷開與byteBuf的關聯;
2)若super.release方法返回false
則直接返回false;
所以super.release方法就成為了關鍵,追溯源碼可以發現,該方法調用路徑是WrappedByteBuf#release方法——>AbstractReferenceCountedByteBuf#release方法,而該方法邏輯是引用計數減1后變為0了,就執行內存釋放,返回true;引用計數減1后不為0則不執行內存釋放,返回fasle;
所以當內存沒有釋放時,不會執行leak.close方法,意味著bytebuf一直被弱引用defaultResourceLeak關聯,此時bytebuf被gc回收后,defaultResourceLeak會被加入到引用隊列中,當下次再次執行toLeakAwareBuffer方法時,會執行reportLeak方法,取出引用隊列中的元素,若存在于set集合中,則可認定super.release返回了false導致沒有執行leak.close方法,歸根結底是引用計數減1沒有為0,所以內存也沒有歸還至池中;
思想
釋放內存,釋放成功,則斷開內存與weakReference的關聯,gc的時候,就不會將WeakReference實例加進隊列中;釋放失敗,則內存與WeakReference之間依舊保持關聯,gc時,就會將weakReference實例加入到隊列中,程序再次分配新內存時,會檢測到隊列中有元素,則可以推斷有內存釋放失敗;
總結
以上是生活随笔為你收集整理的netty源码解读六(内存池相关)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Codeforces Round #38
- 下一篇: 小程序手机号验证码登录