近日在使用Netty框架開發程序中出現了內存泄露的問題,百度加調試了一番,做個整理。
1. ByteBuf分類、回收及使用場景
Netty中按是否使用了池化技術,ByteBuf分為兩類,一類是非池化的ByteBuf,包括UnpooledHeapByteBuf、UnpooledDirectByteBuf等等,每次I/O讀寫都會創建一個新ByteBuf,頻繁進行大塊內存的分配和回收對性能有一定影響,非池化的ByteBuf可以通過JVM GC自動回收,也推薦手動回收UnpooledDirectByteBuf等使用堆外內存的ByteBuf;另一類是池化的ByteBuf,包括pooledHeapByteBuf、pooledDirectByteBuf等等,其先申請一塊大內存池,在內存池中分配空間,對於這種應用級別的內存二次分配,就需要手動對池化的ByteBuf進行釋放,否則就有可能出現內存泄露的問題。
ByteBuf 該如何選擇: 一般業務數據的內存分配選用Java堆內存UnpooledHeapByteBuf,其實現簡單,回收快,不會出現內存管理問題;對於I/O數據的內存分配一般選用池化的直接內存PooledDirectByteBuf,避免Java堆內存到直接內存的拷貝,但使用池化ByteBuf時切記自己分配的內存一定要在用完后手動釋放。
Netty的接收和發送ByteBuf采用的DirectBuffers,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(heap buffers)進行socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然后才寫入socket中。相比堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。
2. ByteBuf的計數器實現
那對於池化的ByteBuf在使用完釋放回池中時,如何知道自己被引用多少次,並且在沒有其他引用的時候被釋放呢?ByteBuf的具備實現類都繼承了AbstractReferenceCountedByteBuf類,該類實現了對計數器的操作功能。當某一操作使得ByteBuf的引用增加時,調用retain()函數,使計數器的值原子增加,當某一操作使得ByteBuf的引用減少時,調用release()函數,使計數器的值原子減少,減少到0便會觸發回收操作。關於AbstractReferenceCountedByteBuf類的源碼分析,請見Netty框架AbstractReferenceCountedByteBuf 源碼分析。
3. ByteBuf計數器的調用場景
(1)當一個ByteBuf新建時,或從另一個ByteBuf創建出獨立個體時(比如copy(),readBytes(int length)),新的ByteBuf在初始化的時候,自己的計數器也會初始化。這種ByteBuf使用結束后,要主動釋放
(2)有些ByteBuf從另一個ByteBuf衍生出來時(比如decode(),retainedSlice(index, length)),底層共用同一個buffer,也會調用retain()函數,來使得計數器增加。使用完畢也要主動釋放。
(3)有些ByteBuf從另一個ByteBuf衍生出來時(比如duplicate(), slice(), order()),底層與父類ByteBuf共用一個buffer,其沒有自己的計數器,共用父類ByteBuf的計數器,計數器也不會增加。因此,如果要把這些衍生ByteBuf傳遞給其他函數時,必須要主動調用retain()函數,並在本函數釋放父類ByteBuf,在下一個函數里釋放衍生ByteBuf。如下面代碼
ByteBuf parent = ctx.alloc().directBuffer(512); parent.writeBytes(...); try { while (parent.isReadable(16)) { ByteBuf derived = parent.readSlice(16); derived.retain(); process(derived); } } finally { parent.release(); } ... public void process(ByteBuf buf) { ... buf.release(); }
以我遇到的內存泄漏的場景為例
(1)readBytes(int length)函數可能會調用到如下代碼,新生成一個ByteBuf
@Override protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) { if (HAS_UNSAFE) { return PooledUnsafeDirectByteBuf.newInstance(maxCapacity); } else { return PooledDirectByteBuf.newInstance(maxCapacity); } }
(2)ByteBuf in = (ByteBuf) super.decode(ctx,inByteBuf) 調用decode函數時,會調用到buffer.retainedSlice(index, length)函數,會返回原ByteBuf的一個片段,同時使原ByteBuf的計數器增加。它們底層共用同一個buffer,修改一個會影響另一個。
final <U extends AbstractPooledDerivedByteBuf> U init( AbstractByteBuf unwrapped, ByteBuf wrapped, int readerIndex, int writerIndex, int maxCapacity) { wrapped.retain(); // Retain up front to ensure the parent is accessible before doing more work. parent = wrapped; rootParent = unwrapped; ...... }
4. 誰來釋放ByteBuf
最基本的規則是誰最后訪問ByteBuf,誰最后負責釋放。需注意的是:
(1)發送組件將ByteBuf傳遞給接收組件,發送組件一般不負責釋放,由接收組件釋放;
(2)如果一個組件除了接收處理ByteBUf,而不做其他操作(比如再傳給其他組件),這個組件負責釋放ByteBuf。
例如
Action | Who should release? |
Who released? |
1. main() creates buf |
buf →main() |
main () releases buf |
2. main() calls a() with buf |
buf →a() |
a() releases buf |
3. a() returns buf merely. |
buf →main() |
main () releases buf |
4. main() calls b() with buf |
buf →b() |
b() releases buf |
5. b() returns the copy of buf |
buf →b() |
b() releases buf, |
6. main() calls c() with copy |
copy →c() |
c() releases copy |
7. c() swallows copy |
copy →c() |
c() releases copy |
5. 在 ChannelHandler負責鏈中,如何釋放
(1)在Inbound messages中
a. 如果ChannelHandler中,只有處理ByteBuf的操作,不會調ctx.fireChannelRead(buf)把ByteBuf傳遞下去,那就要在這個ChannelHandler中釋放ByteBuf。
b. 如果ChannelHandler中,會調ctx.fireChannelRead(buf)把ByteBuf傳遞給下一個ChannelHandler,那在當前ChannelHandler中不需要釋放ByteBuf,由最后一個使用該ByteBuf的ChannelHandler釋放。
c. 如果處理的ByteBuf是由decode()等會增加計數器的操作生成的,不再傳遞時,ByteBuf也要釋放。
d. 如果不確定要不要釋放,或者簡化釋放的過程,可以調用ReferenceCountUtil.release(ByteBuf)函數。
e. 也可以把ChannelHandler都承繼自SimpleChannelInboundHandler虛類,該類會在channelRead函數中調用ReferenceCountUtil.release(msg)來幫助釋放ByteBuf,如下代碼所示,channelRead0(ctx, imsg)是一個虛函數,子類實現channelRead0函數用來完成處理邏輯。
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { if (acceptInboundMessage(msg)) { @SuppressWarnings("unchecked") I imsg = (I) msg; channelRead0(ctx, imsg); } else { release = false; ctx.fireChannelRead(msg); } } finally { if (autoRelease && release) { ReferenceCountUtil.release(msg); } } }
(2)在Outbound messages中
Outbound messages中的ByteBuf都是由應用程序產生的,由Netty負責釋放。
6. 內存泄露檢測
使用引用計數的缺點在於容易產生內存泄露,因為JVM不知道引用計數的存在。當一個對象不可達時,JVM可能會收回該對象,但這個對象的引用計數可能還不是0,這就導致該對象從池里分配的空間不能歸還到池里,從而導致內存泄露。
Netty提供了一種內存泄露檢測機制,可以通過配置參數不同選擇不同的檢測級別,參數設置為java -Dio.netty.leakDetection.level=advanced
DISABLED
:完全禁用內存泄露檢測,不推薦SIMPLE
:抽樣1%的ByteBuf,提示是否有內存泄露ADVANCED
:抽樣1%的ByteBuf,提示哪里產生了內存泄露PARANOID
:對每一個ByteBu進行檢測,提示哪里產生了內存泄露
我在測試時,直接提示了ByteBuf內存泄露的位置,如下,找到自己程序代碼,看哪里有新生成的ByteBuf對象沒有釋放,主動釋放一下,調用對象的release()函數,或者用工具類幫助釋放ReferenceCountUtil.release(msg)。
2020-06-12 17:04:41.242 [nioEventLoopGroup-2-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information. Recent access records: Created at: io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:363) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187) io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:123) io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:872) com.spring.netty.twg.service.TwgMessageDecoder.formatDecoder(TwgMessageDecoder.java:176) com.spring.netty.twg.service.TwgMessageDecoder.getMessageBody(TwgMessageDecoder.java:90) com.spring.netty.twg.service.TwgMessageDecoder.decode(TwgMessageDecoder.java:76) io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:332) io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501)
2020-06-12 17:04:45.460 [nioEventLoopGroup-2-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information. Recent access records: Created at: io.netty.buffer.SimpleLeakAwareByteBuf.unwrappedDerived(SimpleLeakAwareByteBuf.java:143) io.netty.buffer.SimpleLeakAwareByteBuf.retainedSlice(SimpleLeakAwareByteBuf.java:57) io.netty.handler.codec.LengthFieldBasedFrameDecoder.extractFrame(LengthFieldBasedFrameDecoder.java:498) io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:437) com.spring.netty.twg.service.TwgMessageDecoder.decode(TwgMessageDecoder.java:31) io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:332)
參考資料
https://netty.io/wiki/reference-counted-objects.html#who-destroys-it
https://blog.csdn.net/u013202238/article/details/93383887