Netty中ByteBuf內存泄露及釋放解析


近日在使用Netty框架開發程序中出現了內存泄露的問題,百度加調試了一番,做個整理。

直接看解決方法請移步Netty內存泄漏解決ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected

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 bufmain() main() releases buf
2. main() calls a() with buf bufa() a() releases buf
3. a() returns buf merely. bufmain() main() releases buf
4. main() calls b() with buf bufb() b() releases buf
5. b() returns the copy of buf bufb() b() releases buf,main() releases copy
6. main() calls c() with copy copyc() c() releases copy 
7. c() swallows copy copyc() 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM