netty(十二)初識Netty - ByteBuf 內存回收 一、ByteBuf的種類 二、直接內存回收原理 三、內存釋放使用方式

不論我們在前面學習NIO的ByteBuffer,還是現在Netty當中的ByteBuf,其都有使用直接內存的方式。

在Netty當中,我們使用完直接內存,需要去手動進行釋放,而不應該等待GC去進行回收,以減少發生內存溢出的風險。

一、ByteBuf的種類

關於其種類,有很多種,我們根據前面提到的池化機制,將其主要分爲兩大類,每一類噹噹中又分爲堆內存和直接內存:

  • UnpooledHeapByteBuf:非池化堆內存ByteBuf,受JVM內存管理,可以等待GC回收。
  • UnpooledDirectByteBuf:非池化直接內存ByteBuf,不收JVM管理,雖然可以受GC回收,但不是及時的,可能會發生內存溢出,需要手動進行回收。
  • PooledByteBuf:池化ByteBuf,這種有更復雜的回收範式,後面通過源碼分析,具體查看其實現細節。
    • PooledHeapByteBuf:池化堆內存ByteBuf
    • PooledDirectByteBuf:池化直接內存ByteBuf

二、直接內存回收原理

在前面的文章中,我們簡單聊到過ByteBuf的結構:

public abstract class ByteBuf implements ReferenceCounted

如上所示,其實現了ReferenceCounted的接口,接口翻譯過來叫做“引用計數”。

相信學過jvm GC的同學應該有所瞭解“引用計數法”,當一個對象有引用時,我們就對計數器加1,反之就減1,但是引用計數法無法處理環形垃圾,所以後面提出了“根可達算法”,簡單提一下,需要了解細節的朋友可以看我的專題【JVM】。

此處的引用計數,用於ByteBuf的直接內存回收,我們看下其主要的方法:

public interface ReferenceCounted {
    /**
     * 返回當前對象的引用計數
     */
    int refCnt();

    /**
     * 將引用計數增加1
     */
    ReferenceCounted retain();

    /**
     * 按指定的increment增加引用計數
     */
    ReferenceCounted retain(int increment);

    /**
     * 將引用計數減少1,並在引用計數達到0解除分配此對象
     */
    boolean release();

    /**
     * 將引用計數減少指定的decrement ,如果引用計數達到0則取消分配此對象。
     */
    boolean release(int decrement);
}

所有的ByteBuf都會實現這個接口,當一個新的ReferenceCounted被實例化時,它以1的引用計數開始。 retain()增加引用計數,而release()減少引用計數。 如果引用計數減少到0 ,對象將被釋放,並且訪問釋放的對象通常會導致訪問衝突。

通過下面的代碼簡單試用一下:

    public static void main(String[] args) {

        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();

        //打印當前的引用計數
        System.out.println("初始化後的引用計數" + byteBuf.refCnt());

        //釋放引用計數
        byteBuf.release();
        //打印當前的引用計數
        System.out.println("釋放後的引用計數" + byteBuf.refCnt());

        //調用byteBuf
        try {
            byteBuf.writeInt(888);
        } catch (Exception e) {
            System.out.println("釋放後調用異常:" + e);
        }

        //增加引用計數
        try {
            byteBuf.retain();
        } catch (Exception e) {
            System.out.println("釋放後增加引用計數異常:" + e);
        }

        // 重新分配
        byteBuf = ByteBufAllocator.DEFAULT.buffer();
        //調用byteBuf
        byteBuf.writeInt(888);
        System.out.println("重新分配後的引用計數" + byteBuf.refCnt());
    }

結果:

初始化後的引用計數1
釋放後的引用計數0
釋放後調用異常:io.netty.util.IllegalReferenceCountException: refCnt: 0
釋放後增加引用計數異常:io.netty.util.IllegalReferenceCountException: refCnt: 0, increment: 1
重新分配後的引用計數1

當引用計數變爲0後,整個內存就釋放了,再次使用會拋出異常,重新嘗試增加引用計數也會跑出異常,只能進行重新分配。

三、內存釋放使用方式

3.1 手動釋放

前面簡單瞭解了關於內存釋放的內容,那麼我們應該如何使用呢?是不是可以向我們習慣的java代碼一樣,在finally當中調用呢?

try {

} finally {
    byteBuf.release();
}

直接給出結論,是不行的。

前面我們介紹時候就說過,會有機率造成內存溢出的,即使不會發生也會造成內存的浪費。

前面的文章當中,我們學習了Pipeline和Handler。通常我們會將一個byteBuf傳遞給另一個channelHandler去處理,是存在一個傳遞性的。這裏面存在兩種情況:

  • 假設一共有5個channelHandler,在第二個當中,將byteBuf轉換成了java對象,然後將對象傳遞給第三個channelHandler,此時byteBuf就沒有用了,所以此時就應該釋放。
  • 一直以byteBuf傳遞,直到最後一個channelHandler才進行釋放。

總結一句話:最後誰用完了,誰就負責釋放。

建議:如果確定這個buf在最後時刻用完了,而又無法確定當前有多少個引用計數,使用如下兩種方式釋放:

  • 循環調用release(),知道返回true。
  • 通過refCnt()獲取當前的引用計數,然後調用release(int refCnt)釋放。

3.2 tail和head自動釋放

還記得前面將Pipeline和Handler時,提到了關於head和tail的概念,除了我們自己添加的Handler以外,會默認有一個頭和尾的處理器。

在這兩個處理器當中,也會有自動回收內存的保底能力,但是前提是要求我們將byteBuf傳遞到head或tail當中纔行,對於中途就轉換類型的,仍然需要我們自己去釋放資源。

前面我們還學習過入站處理器和出棧處理器,其中入站處理器傳遞內容需要使用channelRead()方法,而在出站處理器傳遞參數需要使用write方法,這將作爲我們跟蹤代碼的標記。

下面我們簡單跟蹤下源碼,看看是如何實現的內存釋放。
我們跟蹤pipeline的addLast方法,跟蹤到了AbstractChannelHandlerContext這個抽象類,其有兩個實現類:

剛好對應我們的head和tail處理器。

3.2.1 TailContext

首先看tail處理器,實現了ChannelInboundHandler,即入站處理器,進行入站首尾工作。

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler

找到channelRead方法:

       public void channelRead(ChannelHandlerContext ctx, Object msg) {
            DefaultChannelPipeline.this.onUnhandledInboundMessage(ctx, msg);
        }

繼續跟蹤onUnhandledInboundMessage

    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug("Discarded inbound message {} that reached at the tail of the pipeline. Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }

    }

發現其中的引用計數工具類,調用了release方法:

ReferenceCountUtil.release(msg);

判斷msg是否是實現了ReferenceCounted ?是就進行是否,否則返回false。

    public static boolean release(Object msg) {
        return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;
    }

3.2.1 HeadContext

查看HeadContext,實現了ChannelOutboundHandler,即出站處理器,進行出站首尾工作。

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler

找到其write方法:

        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            this.unsafe.write(msg, promise);
        }

繼續跟蹤write:

        public final void write(Object msg, ChannelPromise promise) {
            this.assertEventLoop();
            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null) {
                this.safeSetFailure(promise, this.newClosedChannelException(AbstractChannel.this.initialCloseCause));
                ReferenceCountUtil.release(msg);
            } else {
                int size;
                try {
                    msg = AbstractChannel.this.filterOutboundMessage(msg);
                    size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);
                    if (size < 0) {
                        size = 0;
                    }
                } catch (Throwable var6) {
                    this.safeSetFailure(promise, var6);
                    ReferenceCountUtil.release(msg);
                    return;
                }

                outboundBuffer.addMessage(msg, size, promise);
            }
        }

在上面的代碼中,仍然發現了

ReferenceCountUtil.release(msg)

其他代碼此文暫時不做講解了。

無論是head,還是tail,都需要將buf傳遞過來,才能進行釋放。


本文暫時介紹這些,後面繼續,有幫助的話點個贊吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章