netty发送数据_看完这篇还不清楚Netty的内存管理,那我就哭了
說明
在學(xué)習(xí)Netty的時(shí)候,ByteBuf隨處可見,但是如何高效分配ByteBuf還是很復(fù)雜的,Netty的池化內(nèi)存分配這塊還是比較難的,很多人學(xué)習(xí)過,看過但是還是云里霧里的,本篇文章就是主要來講解:Netty分配池化的堆外內(nèi)存的細(xì)節(jié),期待可以讓你明白!!!
由于為了更好的表達(dá),文章中的圖我最少畫了6小時(shí),畫的不熟悉,并且也強(qiáng)調(diào)一些細(xì)節(jié)上。
ByteBuf重要性
ByteBuf在Netty中一直存在,讀寫必備!ByteBuf是Netty的數(shù)據(jù)容器,高效分配ByteBuf至關(guān)重要!
Netty從socket讀取數(shù)據(jù)。
Netty準(zhǔn)備把數(shù)據(jù)寫到socket中去。
通過這里我們就可以看到,再把數(shù)據(jù)寫socket的之前會(huì)判斷是否是堆外內(nèi)存,如果不是會(huì)構(gòu)造一個(gè)directbuffer對(duì)象的,細(xì)節(jié)代碼如下:
if (msg instanceof ByteBuf) { ByteBuf buf = (ByteBuf) msg; if (buf.isDirect()) { return msg; } return newDirectBuffer(buf); }所以本篇文章就是主要來講解:Netty分配池化的堆外內(nèi)存的細(xì)節(jié),其實(shí)分配堆內(nèi)存的細(xì)節(jié)很多也是類似的。
備注: 為什么不是堆外內(nèi)存還要轉(zhuǎn)堆外內(nèi)存,為什么加這個(gè)判斷,我之前也不理解,忽然有天和滌生大佬討論,討論討論就清晰了,后續(xù)有空寫篇。
總覽
本次主要討論的是關(guān)于池化內(nèi)存的分配,PooledByteBufAllocator就是netty分配池化內(nèi)存的操作入口。
其提供對(duì)外常用操作api:
Netty在發(fā)送數(shù)據(jù)的時(shí)候會(huì)判斷是否是堆外內(nèi)存,如果不是會(huì)進(jìn)行封裝的:
所有這里我們以分配池化的堆外內(nèi)存為例,進(jìn)行本文說明。池化的堆內(nèi)存分配其實(shí)流程都差不多的。
下面我們來看看分配示例demo:
public static void main(String[] args) { ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; //tiny規(guī)格內(nèi)存分配 會(huì)變成大于等于16的整數(shù)倍的數(shù):這里254 會(huì)規(guī)格化為256 ByteBuf byteBuf = alloc.directBuffer(254); //讀寫bytebuf byteBuf.writeInt(126); System.out.println(byteBuf.readInt()); //很重要,內(nèi)存釋放 byteBuf.release();}后續(xù)我們都會(huì)根據(jù)這段簡(jiǎn)單的demo進(jìn)行分析。
操作入口類
PooledByteBufAllocator的初始化:
進(jìn)去之后可以看到核心類的一初始化操作:
分配理論是jemalloc,可以理解為java版本的jemalloc實(shí)現(xiàn)。
PoolThreadCache
通過上圖可以清晰的了解到PoolThreadCache的主要數(shù)據(jù)結(jié)構(gòu)。
開始的時(shí)候,這些Cache里面都是沒有值的,只有在調(diào)用free釋放的時(shí)候(在后續(xù)釋放內(nèi)存中會(huì)講解),才會(huì)把之前分配的內(nèi)存大小放到該cache的queue里面,其實(shí)每次分配的時(shí)候都是先看看是否緩存里面有,如果有直接返回,沒有則進(jìn)行正常的分配流程(內(nèi)存分配會(huì)講解)。
我們來看看PoolArena directArena內(nèi)容:
下面我們來看看PoolArena結(jié)構(gòu)。
PoolArena
通過下圖可以清晰的了解到PoolArena的主要數(shù)據(jù)結(jié)構(gòu)。
在PoolArena里面涉及到PoolChunkList和PoolSubpage對(duì)應(yīng)的結(jié)構(gòu)有PoolChunk和PoolSubpage,我們來詳細(xì)的看看這2塊內(nèi)容。
PoolChunk
第一次的時(shí)候,PoolChunkList、PoolSubpage都是默認(rèn)值,需要新增一個(gè)Chunk,默認(rèn)一個(gè)Chunk是16M。內(nèi)部會(huì)結(jié)構(gòu)是完全二叉樹一共有4096個(gè)節(jié)點(diǎn),有2048個(gè)葉子節(jié)點(diǎn)(每個(gè)葉子節(jié)點(diǎn)大小為一個(gè)page,就是8k),非葉子節(jié)點(diǎn)的內(nèi)存大小等于左子樹內(nèi)存大小加上右子樹內(nèi)存大小。
完全二叉樹結(jié)構(gòu)如下:
這顆完全二叉樹在java中是使用數(shù)組來進(jìn)行表示的。
唯一需要注意的是,下標(biāo)是從1開始而不是0.
depthMap的值初始化后不再改變,memoryMap的值則隨著節(jié)點(diǎn)分配而改變。
這個(gè)值太多就不都截圖了,就是把上面那顆完全二叉樹用數(shù)組表示了而已,只是值存的不是節(jié)點(diǎn)的下標(biāo)而是存的樹的深度而已。
depthMap數(shù)組值為0表示可以分配16M空間,如果為1 表示可以分配8M,,如果為2表示嗯可以分配4M,如果為3表示可以分配2M ……………………如果為11表示可以分配8k空間。
如果該節(jié)點(diǎn)已經(jīng)分配完成,就設(shè)置為12即可。
怎么確定需要分配的大小在深度是多少?
如果需要分配的內(nèi)存規(guī)格化之后,是小于8k,那么在8k上面分配即可(即深度為11)。
如果為8k或者大于8k那么通過下面代碼就可以定位到深度了:
int d = maxOrder - (log2(normCapacity) - pageShifts);知道深度之后,怎么進(jìn)行定位到那個(gè)節(jié)點(diǎn)呢???
找到該節(jié)點(diǎn)之后,先把該節(jié)點(diǎn)顯示占用,在更新起父節(jié)點(diǎn)父節(jié)點(diǎn)的父………………如下:
SubpagePool
上面的圖就是關(guān)于SubpagePool的內(nèi)存結(jié)構(gòu)了。我們?cè)诜峙鋚age的時(shí)候,根據(jù)memoryMap對(duì)于的值就知道是否被分配了,那么如果是subpagePool呢?
subpagePool分為2類:tinySubpagePools和smallSubpagePools,大小對(duì)于也對(duì)于上面的圖里面了,每類都是固定大小的,如果分配256b的大小,那么一個(gè)page就是8k,8*1024/256 = 32塊。那么怎么怎么表示每個(gè)還被分配了呢?
private final long[] bitmap;由于一個(gè)long占用的字節(jié)數(shù)為64,我們這里僅僅是需要表示32個(gè),所以使用一個(gè)long即可了,二進(jìn)制每位 1表示已經(jīng)使用了,0表示還未使用。
由于subpage不僅僅需要定位到完全二叉樹在那個(gè)節(jié)點(diǎn),還需要知道在long的第幾個(gè) 并且是第幾位,所以要復(fù)雜一些:
通過一個(gè)long的前32位來表示subpage的第幾個(gè)long的第幾位上面,通過后32來表示在完全二叉樹的那個(gè)
分配核心
分配入口:ByteBuf byteBuf = alloc.directBuffer(256);
進(jìn)行跟進(jìn)代碼:
我們來看:PooledByteBuf buf = newByteBuf(maxCapacity);
構(gòu)建PooledByteBuf對(duì)象。最后返回PooledByteBuf對(duì)象。
我們來看下類繼承結(jié)構(gòu):
所有ByteBuf byteBuf = alloc.directBuffer(256);這句話是沒有什么問題的,不會(huì)報(bào)錯(cuò)。
我們來看看newByteBuf(maxCapacity)的細(xì)節(jié)實(shí)現(xiàn):
這里借助了Netty增加實(shí)現(xiàn)的Recycler對(duì)象池技術(shù)。Recycler設(shè)計(jì)也非常精巧,后續(xù)可以專門寫篇Recycler文章,今天不是重點(diǎn),我們只要知道由于分配PolledByteBuf對(duì)象的代價(jià)有點(diǎn)大,如果需要頻繁使用到PolledByteBuf對(duì)象,并且對(duì)性能有所要求,那么池化技術(shù)是一個(gè)不錯(cuò)的選擇(比如我們以前使用的線程池、數(shù)據(jù)庫連接池等都是類似道理),池化技術(shù)在一定程度上面減少了頻繁創(chuàng)建對(duì)象帶來的性能開銷。其實(shí)這個(gè)類似的思想非常常見(比如我們查詢數(shù)據(jù)庫成本高,緩存到redis,思路也是一樣的),在本篇后續(xù)中還可以體會(huì)到(PoolThreadCache)。
通過PooledByteBuf buf = newByteBuf(maxCapacity);僅僅是獲取到了一個(gè)初始對(duì)象而已。
分配的核心在:allocate(cache, buf, reqCapacity);
- 先嘗試在1步驟 進(jìn)行分配,根據(jù)不同的類型定位到不同的Caches,如果有進(jìn)行分配直接返回。
- 如果1步驟 分配不了,進(jìn)行2步驟上面分配。
2步驟分配細(xì)節(jié):看看需要分配的是什么類型 page還是subpage,如果是subpage在根據(jù)看看是tinySubpagePools還是smallSubpagePools,找到對(duì)應(yīng)的槽位,看看鏈表里是否有可用的PoolSubpage,如果有就進(jìn)行分配修改標(biāo)記退出,如果沒有就現(xiàn)需要在先分配一個(gè)page了,根據(jù)chunklist的這些看看是否有合適的,如果有合適的,那么在這些已經(jīng)有的chunk上面進(jìn)行分配一個(gè)page (分配page也是這個(gè)情況了)
之后在根據(jù)分配到的page,進(jìn)行該請(qǐng)求大小的分配 (由于一個(gè)page可以存儲(chǔ)很多同大小的數(shù)量)需要用long的位標(biāo)記,表示該位置分配了,并且修改完全二叉樹的父等值,分配結(jié)束。如果沒有chunk那么需要新分配一塊chunk之后重復(fù)上面步驟即可。
釋放核心
釋放入口 :byteBuf.release();
進(jìn)行跟進(jìn)代碼:
通過這段代碼我們就這段放入到相應(yīng)的queue了:
緩存到了對(duì)應(yīng)的Cache的queue里面了。
文章來源:https://dwz.cn/ESnnz4zJ
作者:零度冰炫
總結(jié)
以上是生活随笔為你收集整理的netty发送数据_看完这篇还不清楚Netty的内存管理,那我就哭了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: erp生产管理系统流程_企业生产管理好帮
- 下一篇: tftpd32