Netty內(nèi)存池泄漏問題以解決方案
為了提升消息接收和發(fā)送性能,Netty針對(duì)ByteBuf的申請(qǐng)和釋放采用池化技術(shù),通過PooledByteBufAllocator可以創(chuàng)建基于內(nèi)存池分配的ByteBuf對(duì)象,這樣就避免了每次消息讀寫都申請(qǐng)和釋放ByteBuf。由于ByteBuf涉及byte[]數(shù)組的創(chuàng)建和銷毀,對(duì)于性能要求苛刻的系統(tǒng)而言,重用ByteBuf帶來的性能收益是非??捎^的。
內(nèi)存池是一把雙刃劍,如果使用不當(dāng),很容易帶來內(nèi)存泄漏和內(nèi)存非法引用等問題,另外,除了內(nèi)存池,Netty同時(shí)也支持非池化的ByteBuf,多種類型的ByteBuf功能存在一些差異,使用不當(dāng)很容易帶來各種問題。
業(yè)務(wù)路由分發(fā)模塊使用Netty作為通信框架,負(fù)責(zé)協(xié)議消息的接入和路由轉(zhuǎn)發(fā),在功能測(cè)試時(shí)沒有發(fā)現(xiàn)問題,轉(zhuǎn)性能測(cè)試之后,運(yùn)行一段時(shí)間就發(fā)現(xiàn)內(nèi)存分配異常,服務(wù)端無法接收請(qǐng)求消息,系統(tǒng)吞吐量降為0。
1 路由轉(zhuǎn)發(fā)服務(wù)代碼
作為案例示例,對(duì)業(yè)務(wù)服務(wù)路由轉(zhuǎn)發(fā)代碼進(jìn)行簡(jiǎn)化,以方便分析:
public class RouterServerHandler extends ChannelInboundHandlerAdapter { static ExecutorService executorService = Executors.newSingleThreadExecutor(); PooledByteBufAllocator allocator = new PooledByteBufAllocator(false); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf reqMsg = (ByteBuf)msg; byte [] body = new byte[reqMsg.readableBytes()]; executorService.execute(()->{ //解析請(qǐng)求消息,做路由轉(zhuǎn)發(fā),代碼省略 //轉(zhuǎn)發(fā)成功,返回響應(yīng)給客戶端 ByteBuf respMsg = allocator.heapBuffer(body.length); respMsg.writeBytes(body);//作為示例,簡(jiǎn)化處理,將請(qǐng)求返回 ctx.writeAndFlush(respMsg); }); } }
進(jìn)行一段時(shí)間的性能測(cè)試之后,日志中出現(xiàn)異常,進(jìn)程內(nèi)存不斷飆升,懷疑存在內(nèi)存泄漏問題,如下圖所示。
2 響應(yīng)消息內(nèi)存釋放玄機(jī)
對(duì)業(yè)務(wù)ByteBuf申請(qǐng)相關(guān)代碼進(jìn)行排查,發(fā)現(xiàn)響應(yīng)消息由業(yè)務(wù)線程創(chuàng)建,但是卻沒有主動(dòng)釋放,因此懷疑是響應(yīng)消息沒有釋放導(dǎo)致的內(nèi)存泄漏。
因?yàn)轫憫?yīng)消息使用的是PooledHeapByteBuf,如果發(fā)生內(nèi)存泄漏,利用堆內(nèi)存監(jiān)控就可以找到泄漏點(diǎn),通過Java VisualVM工具觀察堆內(nèi)存占用趨勢(shì),并沒有發(fā)現(xiàn)堆內(nèi)存發(fā)生泄漏,如下圖所示。
對(duì)內(nèi)存做快照,查看在性能壓測(cè)過程中響應(yīng)消息PooledUnsafeHeapByteBuf的實(shí)例個(gè)數(shù),如下圖所示,響應(yīng)消息對(duì)象個(gè)數(shù)和內(nèi)存占用都很少,排除內(nèi)存泄漏嫌疑。
業(yè)務(wù)從內(nèi)存池中申請(qǐng)了ByteBuf,但是卻沒有主動(dòng)釋放它,最后也沒有發(fā)生內(nèi)存泄漏,這究竟是什么原因呢?
通過對(duì)Netty源碼的分析,我們破解了其中的玄機(jī)。
原來調(diào)用ctx.writeAndFlush(respMsg)方法時(shí),當(dāng)消息發(fā)送完成,Netty框架會(huì)主動(dòng)幫助應(yīng)用釋放內(nèi)存,內(nèi)存的釋放分為如下兩種場(chǎng)景。
(1)如果是堆內(nèi)存(PooledHeapByteBuf),則將HeapByteBuffer轉(zhuǎn)換成DirectByteBuffer,并釋放PooledHeapByteBuf到內(nèi)存池,代碼如下(AbstractNioChannel類):
protected final ByteBuf newDirectBuffer(ByteBuf buf) { final int readableBytes = buf.readableBytes(); if (readableBytes == 0) { ReferenceCountUtil.safeRelease(buf); return Unpooled.EMPTY_BUFFER; } final ByteBufAllocator alloc = alloc(); if (alloc.isDirectBufferPooled()) { ByteBuf directBuf = alloc.directBuffer(readableBytes); directBuf.writeBytes(buf, buf.readerIndex(), readableBytes); ReferenceCountUtil.safeRelease(buf); return directBuf; } }
如果消息完整地被寫到SocketChannel中,則釋放DirectByteBuffer,代碼如下(ChannelOutboundBuffer):
public boolean remove() { Entry e = flushedEntry; if (e == null) { clearNioBuffers(); return false; } Object msg = e.msg; ChannelPromise promise = e.promise; int size = e.pendingSize; removeEntry(e); if (!e.cancelled) { ReferenceCountUtil.safeRelease(msg); safeSuccess(promise); decrementPendingOutboundBytes(size, false, true); } }
對(duì)Netty源碼進(jìn)行斷點(diǎn)調(diào)試,驗(yàn)證上述分析。
斷點(diǎn)1:在響應(yīng)消息發(fā)送處設(shè)置斷點(diǎn),獲取到的PooledUnsafeHeapByteBuf實(shí)例的ID為1506,如下圖所示。
斷點(diǎn)2:在HeapByteBuffer轉(zhuǎn)換成DirectByteBuffer處設(shè)置斷點(diǎn),發(fā)現(xiàn)實(shí)例ID為1506的PooledUnsafeHeapByteBuf被釋放,如下圖所示。
斷點(diǎn)3:轉(zhuǎn)換之后待發(fā)送的響應(yīng)消息PooledUnsafeDirectByteBuf實(shí)例的ID為1527,如下圖所示。
斷點(diǎn)4:在響應(yīng)消息發(fā)送完成后,實(shí)例ID為1527的PooledUnsafeDirectByteBuf被釋放到內(nèi)存池中,如下圖所示。
(2)如果是DirectByteBuffer,則不需要轉(zhuǎn)換,在消息發(fā)送完成后,由ChannelOutboundBuffer的remove()負(fù)責(zé)釋放。
通過源碼解讀、調(diào)試及堆內(nèi)存的監(jiān)控分析,可以確認(rèn)不是響應(yīng)消息沒有主動(dòng)釋放導(dǎo)致的內(nèi)存泄漏,需要Dump內(nèi)存做進(jìn)一步定位。
3 采集堆內(nèi)存快照分析
執(zhí)行jmap命令,Dump應(yīng)用內(nèi)存堆棧,如圖8所示。
通過MemoryAnalyzer工具對(duì)內(nèi)存堆棧進(jìn)行分析,尋找內(nèi)存泄漏點(diǎn),如圖9所示。
從下圖可以看出,內(nèi)存泄漏點(diǎn)是Netty內(nèi)存池對(duì)象PoolChunk,由于請(qǐng)求和響應(yīng)消息內(nèi)存分配都來自PoolChunk,暫時(shí)還不確認(rèn)是請(qǐng)求還是響應(yīng)消息導(dǎo)致的問題。
進(jìn)一步對(duì)代碼進(jìn)行分析,發(fā)現(xiàn)響應(yīng)消息使用的是堆內(nèi)存HeapByteBuffer,請(qǐng)求消息使用的是DirectByteBuffer,由于Dump出來的是堆內(nèi)存,如果是堆內(nèi)存泄漏,Dump出來的內(nèi)存文件應(yīng)該包含大量的PooledHeapByteBuf,實(shí)際上并沒有,因此可以確認(rèn)系統(tǒng)發(fā)生了堆外內(nèi)存泄漏,即請(qǐng)求消息沒有被釋放或者沒有被及時(shí)釋放導(dǎo)致的內(nèi)存泄漏。
對(duì)請(qǐng)求消息的內(nèi)存分配進(jìn)行分析,發(fā)現(xiàn)在NioByteUnsafe的read方法中申請(qǐng)了內(nèi)存,代碼如下(NioByteUnsafe):
public final void read() { final ChannelConfig config = config(); if (shouldBreakReadReady(config)) { clearReadPending(); return; } final ChannelPipeline pipeline = pipeline(); final ByteBufAllocator allocator = config.getAllocator(); final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle(); allocHandle.reset(config); ByteBuf byteBuf = null; boolean close = false; //代碼省略
繼續(xù)對(duì)allocate方法進(jìn)行分析,發(fā)現(xiàn)調(diào)用的是DefaultMaxMessagesRecvByteBuf- Allocator$MaxMessageHandle的allocate方法,代碼如下(DefaultMaxMessagesRecvByteBuf- Allocator):
public ByteBuf allocate(ByteBufAllocator alloc) { return alloc.ioBuffer(guess()); }
alloc.ioBuffer方法最終會(huì)調(diào)用PooledByteBufAllocator的newDirectBuffer方法創(chuàng)建PooledDirectByteBuf對(duì)象。
請(qǐng)求ByteBuf的創(chuàng)建分析完,繼續(xù)分析它的釋放操作,由于業(yè)務(wù)的RouterServerHandler繼承自ChannelInboundHandlerAdapter,它的channelRead(ChannelHandlerContext ctx, Object msg)方法執(zhí)行完成,ChannelHandler的執(zhí)行就結(jié)束了,代碼示例如下:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf reqMsg = (ByteBuf)msg; byte [] body = new byte[reqMsg.readableBytes()]; executorService.execute(()-> { //解析請(qǐng)求消息,做路由轉(zhuǎn)發(fā),代碼省略 //轉(zhuǎn)發(fā)成功,返回響應(yīng)給客戶端 ByteBuf respMsg = allocator.heapBuffer(body.length); respMsg.writeBytes(body);//作為示例,簡(jiǎn)化處理,將請(qǐng)求返回 ctx.writeAndFlush(respMsg); }); }
通過代碼分析發(fā)現(xiàn),請(qǐng)求ByteBuf被Netty框架申請(qǐng)后竟然沒有被釋放,為了驗(yàn)證分析,在業(yè)務(wù)代碼中調(diào)用ReferenceCountUtil的release方法進(jìn)行內(nèi)存釋放操作,代碼修改如下:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf reqMsg = (ByteBuf)msg;byte [] body = new byte[reqMsg.readableBytes()]; ReferenceCountUtil.release(reqMsg); //后續(xù)代碼省略
修改之后繼續(xù)進(jìn)行壓測(cè),發(fā)現(xiàn)系統(tǒng)運(yùn)行平穩(wěn),沒有發(fā)生OOM異常。對(duì)內(nèi)存活動(dòng)對(duì)象進(jìn)行排序,沒有再發(fā)現(xiàn)大量的PoolChunk對(duì)象,內(nèi)存泄漏問題解決,問題修復(fù)之后的內(nèi)存快照如下圖所示。
4 ByteBuf申請(qǐng)和釋放的理解誤區(qū)
有一種說法認(rèn)為Netty框架分配的ByteBuf框架會(huì)自動(dòng)釋放,業(yè)務(wù)不需要釋放;業(yè)務(wù)創(chuàng)建的ByteBuf則需要自己釋放,Netty框架不會(huì)釋放。
通過前面的案例分析和驗(yàn)證,我們可以看出這個(gè)觀點(diǎn)是錯(cuò)誤的。為了在實(shí)際項(xiàng)目中更好地管理ByteBuf,下面我們分4種場(chǎng)景進(jìn)行說明。
1.基于內(nèi)存池的請(qǐng)求ByteBuf
這類ByteBuf主要包括PooledDirectByteBuf和PooledHeapByteBuf,它由Netty的NioEventLoop線程在處理Channel的讀操作時(shí)分配,需要在業(yè)務(wù)ChannelInboundHandler處理完請(qǐng)求消息之后釋放(通常在解碼之后),它的釋放有兩種策略。
策略1 業(yè)務(wù)ChannelInboundHandler繼承自SimpleChannelInboundHandler,實(shí)現(xiàn)它的抽象方法channelRead0(ChannelHandlerContext ctx, I msg),ByteBuf的釋放業(yè)務(wù)不用關(guān)心,由SimpleChannelInboundHandler負(fù)責(zé)釋放,相關(guān)代碼如下(SimpleChannelInboundHandler):
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { if (acceptInboundMessage(msg)) { I imsg = (I) msg; channelRead0(ctx, imsg); } else { release = false; ctx.fireChannelRead(msg); } } finally { if (autoRelease && re lease) { ReferenceCountUtil.release(msg); } } }
如果當(dāng)前業(yè)務(wù)ChannelInboundHandler需要執(zhí)行,則調(diào)用channelRead0之后執(zhí)行ReferenceCountUtil.release(msg)釋放當(dāng)前請(qǐng)求消息。如果沒有匹配上需要繼續(xù)執(zhí)行后續(xù)的ChannelInboundHandler,則不釋放當(dāng)前請(qǐng)求消息,調(diào)用ctx.fireChannelRead(msg)驅(qū)動(dòng)ChannelPipeline繼續(xù)執(zhí)行。
對(duì)案例中的問題代碼進(jìn)行修改,繼承自SimpleChannelInboundHandler,即便業(yè)務(wù)不釋放請(qǐng)求的ByteBuf對(duì)象,依然不會(huì)發(fā)生內(nèi)存泄漏,修改之后的代碼如下(RouterServerHandlerV2):
public class RouterServerHandlerV2 extends SimpleChannelInboundHandler <ByteBuf> { @Override public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { byte [] body = new byte[msg.readableBytes()]; executorService.execute(()-> { //解析請(qǐng)求消息,做路由轉(zhuǎn)發(fā),代碼省略 //轉(zhuǎn)發(fā)成功,返回響應(yīng)給客戶端 ByteBuf respMsg = allocator.heapBuffer(body.length); respMsg.writeBytes(body);//作為示例,簡(jiǎn)化處理,將請(qǐng)求返回 ctx.writeAndFlush(respMsg); }); }
對(duì)修改之后的代碼做性能測(cè)試,發(fā)現(xiàn)內(nèi)存占用平穩(wěn),無內(nèi)存泄漏問題,驗(yàn)證了之前的分析結(jié)論。
策略2 在業(yè)務(wù)ChannelInboundHandler中調(diào)用ctx.fireChannelRead(msg)方法,讓請(qǐng)求消息繼續(xù)向后執(zhí)行,直到調(diào)用DefaultChannelPipeline的內(nèi)部類TailContext,由它來負(fù)責(zé)釋放請(qǐng)求消息,代碼如下(TailContext):
protected void onUnhandledInboundMessage(Object msg) { try { logger.debug( "Discarded inbound message {} that reached at the tail of thpipeline." + "Please check your pipeline configuration.", msg); } finally { ReferenceCountUtil.release(msg); } }
2.基于非內(nèi)存池的請(qǐng)求ByteBuf
如果業(yè)務(wù)使用非內(nèi)存池模式覆蓋Netty默認(rèn)的內(nèi)存池模式創(chuàng)建請(qǐng)求ByteBuf,例如通過如下代碼修改內(nèi)存申請(qǐng)策略為Unpooled:
//代碼省略 childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); ch.config().setAllocator(UnpooledByteBufAllocator.DEFAULT); p.addLast(new RouterServerHandler()); } }
也需要按照內(nèi)存池的方式釋放內(nèi)存。
3.基于內(nèi)存池的響應(yīng)ByteBuf
根據(jù)之前的分析,只要調(diào)用了writeAndFlush或者flush方法,在消息發(fā)送完成后都會(huì)由Netty框架進(jìn)行內(nèi)存釋放,業(yè)務(wù)不需要主動(dòng)釋放內(nèi)存。
4.基于非內(nèi)存池的響應(yīng)ByteBuf
無論是基于內(nèi)存池還是非內(nèi)存池分配的ByteBuf,如果是堆內(nèi)存,則將堆內(nèi)存轉(zhuǎn)換成堆外內(nèi)存,然后釋放HeapByteBuffer,待消息發(fā)送完成,再釋放轉(zhuǎn)換后的DirectByteBuf;如果是DirectByteBuffer,則不需要轉(zhuǎn)換,待消息發(fā)送完成之后釋放。因此對(duì)于需要發(fā)送的響應(yīng)ByteBuf,由業(yè)務(wù)創(chuàng)建,但是不需要由業(yè)務(wù)來釋放
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java實(shí)現(xiàn)線程的暫停和恢復(fù)的示例詳解
這幾天的項(xiàng)目中,客戶給了個(gè)需求,希望我可以開啟一個(gè)任務(wù),想什么時(shí)候暫停就什么時(shí)候暫停,想什么時(shí)候開始就什么時(shí)候開始,所以本文小編給大家介紹了Java實(shí)現(xiàn)線程的暫停和恢復(fù)的示例,需要的朋友可以參考下2023-11-11JavaWeb ServletConfig作用及原理分析講解
ServletConfig對(duì)象,叫Servlet配置對(duì)象。主要用于加載配置文件的初始化參數(shù)。我們知道一個(gè)Web應(yīng)用里面可以有多個(gè)servlet,如果現(xiàn)在有一份數(shù)據(jù)需要傳給所有的servlet使用,那么我們就可以使用ServletContext對(duì)象了2022-10-10java shiro實(shí)現(xiàn)退出登陸清空緩存
本篇文章主要介紹了java shiro實(shí)現(xiàn)退出登陸清空緩存,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02一篇文章帶你深入理解JVM虛擬機(jī)讀書筆記--鎖優(yōu)化
這篇文章深入介紹了JVM虛擬機(jī)的鎖優(yōu)化,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2021-09-09JAVA 獲取系統(tǒng)當(dāng)前時(shí)間實(shí)例代碼
這篇文章主要介紹了JAVA 獲取系統(tǒng)當(dāng)前時(shí)間實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10SpringBoot如何實(shí)現(xiàn)持久化登錄狀態(tài)獲取
這篇文章主要介紹了SpringBoot 如何實(shí)現(xiàn)持久化登錄狀態(tài)獲取,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java的Spring框架中AOP項(xiàng)目的一般配置和部署教程
這篇文章主要介紹了Java的Spring框架中AOP項(xiàng)目的一般配置和部署教程,AOP面向方面編程的項(xiàng)目部署結(jié)構(gòu)都比較類似,因而也被看作是Spring的一種設(shè)計(jì)模式使用,需要的朋友可以參考下2016-04-04