看完這篇還不清楚Netty的記憶體管理,那我就哭了!
- 2019 年 10 月 3 日
- 筆記
說明
在學習Netty的時候,ByteBuf隨處可見,但是如何高效分配ByteBuf還是很複雜的,Netty的池化記憶體分配這塊還是比較難的,很多人學習過,看過但是還是雲里霧裡的,本篇文章就是主要來講解:Netty分配池化的堆外記憶體的細節,期待可以讓你明白!!!
由於為了更好的表達,文章中的圖我最少畫了6小時,畫的不熟悉,並且也強調一些細節上。
由於該源碼中涉及到大量的二進位操作,建議看看我之前寫的2篇二進位文章:java二進位相關基礎,二進位實戰技巧。
ByteBuf重要性
ByteBuf在Netty中一直存在,讀寫必備!ByteBuf是Netty的數據容器,高效分配ByteBuf至關重要!
Netty從socket讀取數據。
Netty準備把數據寫到socket中去。
通過這裡我們就可以看到,再把數據寫socket的之前會判斷是否是堆外記憶體,如果不是會構造一個directbuffer對象的,細節程式碼如下:
if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; if (buf.isDirect()) { return msg; } return newDirectBuffer(buf); }
所以本篇文章就是主要來講解:Netty分配池化的堆外記憶體的細節,其實分配堆記憶體的細節很多也是類似的。
備註: 為什麼不是堆外記憶體還要轉堆外記憶體,為什麼加這個判斷,我之前也不理解,忽然有天和滌生大佬討論,討論討論就清晰了,後續有空寫篇。
總覽
本次主要討論的是關於池化記憶體的分配,PooledByteBufAllocator就是netty分配池化記憶體的操作入口。
其提供對外常用操作api:
Netty在發送數據的時候會判斷是否是堆外記憶體,如果不是會進行封裝的:
所有這裡我們以分配池化的堆外記憶體為例,進行本文說明。池化的堆記憶體分配其實流程都差不多的。
下面我們來看看分配示例demo:
public static void main(String[] args) { ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; //tiny規格記憶體分配 會變成大於等於16的整數倍的數:這裡254 會規格化為256 ByteBuf byteBuf = alloc.directBuffer(254); //讀寫bytebuf byteBuf.writeInt(126); System.out.println(byteBuf.readInt()); //很重要,記憶體釋放 byteBuf.release(); }
後續我們都會根據這段簡單的demo進行分析。
操作入口類
PooledByteBufAllocator的初始化:
進去之後可以看到核心類的一初始化操作:
分配理論是jemalloc,可以理解為java版本的jemalloc實現。
PoolThreadCache
通過上圖可以清晰的了解到PoolThreadCache的主要數據結構。
開始的時候,這些Cache裡面都是沒有值的,只有在調用free釋放的時候(在後續釋放記憶體中會講解),才會把之前分配的記憶體大小放到該cache的queue裡面,其實每次分配的時候都是先看看是否快取裡面有,如果有直接返回,沒有則進行正常的分配流程(記憶體分配會講解)。
我們來看看PoolArena
下面我們來看看PoolArena結構。
PoolArena
通過下圖可以清晰的了解到PoolArena的主要數據結構。
在PoolArena裡面涉及到PoolChunkList和PoolSubpage對應的結構有PoolChunk和PoolSubpage,我們來詳細的看看這2塊內容。
PoolChunk
第一次的時候,PoolChunkList、PoolSubpage都是默認值,需要新增一個Chunk,默認一個Chunk是16M。內部會結構是完全二叉樹一共有4096個節點,有2048個葉子節點(每個葉子節點大小為一個page,就是8k),非葉子節點的記憶體大小等於左子樹記憶體大小加上右子樹記憶體大小。
完全二叉樹結構如下:
這顆完全二叉樹在java中是使用數組來進行表示的。
唯一需要注意的是,下標是從1開始而不是0.
depthMap
的值初始化後不再改變,memoryMap
的值則隨著節點分配而改變。
這個值太多就不都截圖了,就是把上面那顆完全二叉樹用數組表示了而已,只是值存的不是節點的下標而是存的樹的深度而已。
depthMap數組值為0表示可以分配16M空間,如果為1 表示可以分配8M,,如果為2表示嗯可以分配4M,如果為3表示可以分配2M ……………………如果為11表示可以分配8k空間。
如果該節點已經分配完成,就設置為12即可。
怎麼確定需要分配的大小在深度是多少?
如果需要分配的記憶體規格化之後,是小於8k,那麼在8k上面分配即可(即深度為11)。
如果為8k或者大於8k那麼通過下面程式碼就可以定位到深度了:
int d = maxOrder - (log2(normCapacity) - pageShifts);
知道深度之後,怎麼進行定位到那個節點呢???
找到該節點之後,先把該節點顯示佔用,在更新起父節點父節點的父………………如下:
SubpagePool
上面的圖就是關於SubpagePool的記憶體結構了。我們在分配page的時候,根據memoryMap對於的值就知道是否被分配了,那麼如果是subpagePool呢?
subpagePool分為2類:tinySubpagePools和smallSubpagePools,大小對於也對於上面的圖裡面了,每類都是固定大小的,如果分配256b的大小,那麼一個page就是8k,8*1024/256 = 32塊。那麼怎麼怎麼表示每個還被分配了呢?
private final long[] bitmap;
由於一個long佔64位,我們這裡僅僅是需要表示32個,所以使用一個long即可了,二進位每位 1表示已經使用了,0表示還未使用。
由於subpage不僅僅需要定位到完全二叉樹在那個節點,還需要知道在long的第幾個 並且是第幾位,所以要複雜一些:
通過一個long的前32位來表示subpage的第幾個long的第幾位上面,通過後32來表示在完全二叉樹的那個節點上面,完美。
分配核心
分配入口:ByteBuf byteBuf = alloc.directBuffer(256);
進行跟進程式碼:
我們來看:PooledByteBuf
構建PooledByteBuf對象。最後返回PooledByteBuf對象。
我們來看下類繼承結構:
所有ByteBuf byteBuf = alloc.directBuffer(256);這句話是沒有什麼問題的,不會報錯。
我們來看看newByteBuf(maxCapacity)的細節實現:
這裡藉助了Netty增加實現的Recycler對象池技術。Recycler設計也非常精巧,後續可以專門寫篇Recycler文章,今天不是重點,我們只要知道由於分配PolledByteBuf對象的代價有點大,如果需要頻繁使用到PolledByteBuf對象,並且對性能有所要求,那麼池化技術是一個不錯的選擇(比如我們以前使用的執行緒池、資料庫連接池等都是類似道理),池化技術在一定程度上面減少了頻繁創建對象帶來的性能開銷。其實這個類似的思想非常常見(比如我們查詢資料庫成本高,快取到redis,思路也是一樣的),在本篇後續中還可以體會到(PoolThreadCache)。
通過PooledByteBuf
分配的核心在:allocate(cache, buf, reqCapacity);
- 先嘗試在
進行分配,根據不同的類型定位到不同的Caches,如果有進行分配直接返回。
- 如果
分配不了,進行
上面分配。
步驟分配細節:看看需要分配的是什麼類型 page還是subpage,如果是subpage在根據看看是tinySubpagePools還是smallSubpagePools,找到對應的槽位,看看鏈表裡是否有可用的PoolSubpage,如果有就進行分配修改標記退出,如果沒有就現需要在先分配一個page了,根據chunklist的這些看看是否有合適的,如果有合適的,那麼在這些已經有的chunk上面進行分配一個page (分配page也是這個情況了)
之後在根據分配到的page,進行該請求大小的分配 (由於一個page可以存儲很多同大小的數量)需要用long的位標記,表示該位置分配了,並且修改完全二叉樹的父等值,分配結束。如果沒有chunk那麼需要新分配一塊chunk之後重複上面步驟即可。
釋放核心
釋放入口 : byteBuf.release();
進行跟進程式碼:
通過這段程式碼我們就這段放入到相應的queue了:
快取到了對應的Cache的queue裡面了。
文章github源程式碼地址:nettydemo,或者公號回復「Netty」獲取源碼地址。
如果讀完覺得有收穫的話,歡迎點贊、關注、加公眾號 [匠心零度] ,查閱更多精彩歷史!!!