netty系列之:JVM中的Reference count原來netty中也有

flydean發表於2022-02-14

簡介

為什麼世界上有這麼多JAVA的程式設計師呢?其中一個很重要的原因就是JAVA相對於C++而言,不需要考慮物件的釋放,一切都是由垃圾回收器來完成的。在崇尚簡單的現代程式設計世界中,會C++的高手越來越少,會JAVA的程式設計師越來越多。

JVM的垃圾回收器中一個很重要的概念就是Reference count,也就是物件的引用計數,用來控制物件是否還被引用,是否可以被垃圾回收。

netty也是執行在JVM中的,所以JVM中的物件引用計數也適用於netty中的物件。這裡我們說的物件引用指的是netty中特定的某些物件,通過物件的引用計數來判斷這些物件是否還被使用,如果不再被使用的話就可以把它們(或它們的共享資源)返回到物件池(或物件分配器)。

這就叫做netty的物件引用計數技術,其中一個最關鍵的物件就是ByteBuf。

ByteBuf和ReferenceCounted

netty中的物件引用計數是從4.X版本開始的,ByteBuf是其中最終要的一個應用,它利用引用計數來提高分配和釋放效能.

先來看一下ByteBuf的定義:

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>

可以看到ByteBuf是一個抽象類,它實現了ReferenceCounted的介面。

ReferenceCounted就是netty中物件引用的基礎,它定義了下面幾個非常重要的方法,如下所示:

int refCnt();

ReferenceCounted retain();

ReferenceCounted retain(int increment);

boolean release();

boolean release(int decrement);

其中refCnt返回的是當前引用個數,retain用來增加引用,而release用來釋放引用。

ByteBuf的基本使用

剛分配情況下ByteBuf的引用個數是1:

ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

當呼叫他的release方法之後,refCnt就變成了0:

boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

當呼叫它的retain方法,refCnt就會加一:

ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
buf.retain();
assert buf.refCnt() == 2;
要注意的是,如果ByteBuf的refCnt已經是0了,就表示這個ByteBuf準備被回收了,如果再呼叫其retain方法,則會丟擲IllegalReferenceCountException:refCnt: 0, increment: 1

所以我們必須在ByteBuf還未被回收之前呼叫retain方法。

既然refCnt=0的情況下,不能呼叫retain()方法,那麼其他的方法能夠呼叫嗎?

我們來嘗試呼叫一下writeByte方法:

        try {
            buf.writeByte(10);
        } catch (IllegalReferenceCountException e) {
            log.error(e.getMessage(),e);
        }

可以看到,如果refCnt=0的時候,呼叫它的writeByte方法會丟擲IllegalReferenceCountException異常。

這樣看來,只要refCnt=0,說明這個物件已經被回收了,不能夠再使用了。

ByteBuf的回收

既然ByteBuf中儲存的有refCnt,那麼誰來負責ByteBuf的回收呢?

netty的原則是誰消費ByteBuf,誰就負責ByteBuf的回收工作。

在實際的工作中,ByteBuf會在channel中進行傳輸,根據誰消費誰負責銷燬的原則,接收ByteBuf的一方,如果消費了ByteBuf,則需要將其回收。

這裡的回收指的是呼叫ByteBuf的release()方法。

ByteBuf的衍生方法

ByteBuf可以從一個parent buff中衍生出很多子buff。這些子buff並沒有自己的reference count,它們的引用計數是和parent buff共享的,這些提供衍生buff的方法有:ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder)。

buf = directBuffer();
        ByteBuf derived = buf.duplicate();
        assert buf.refCnt() == 1;
        assert derived.refCnt() == 1;

因為衍生的byteBuf和parent buff共享引用計數,所以如果要將衍生的byteBuf傳給其他的流程進行處理的話,需要呼叫retain()方法:

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();
}

ChannelHandler中的引用計數

netty根據是讀訊息還是寫訊息,可以分為InboundChannelHandler和OutboundChannelHandler,分別用來讀訊息和寫訊息。

根據誰消費,誰釋放的原則,對Inbound訊息來說,讀取完畢之後,需要呼叫ByteBuf的release方法:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        ...
    } finally {
        buf.release();
    }
}

但是如果你只是將byteBuf重發到channel中供其他的步驟進行處理,則不需要release:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    ...
    ctx.fireChannelRead(buf);
}

同樣的在Outbound中,如果只是簡單的重發,則不需要release:

public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
    System.err.println("Writing: " + message);
    ctx.write(message, promise);
}

如果是處理了訊息,則需要release:

public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
    if (message instanceof HttpContent) {
        // Transform HttpContent to ByteBuf.
        HttpContent content = (HttpContent) message;
        try {
            ByteBuf transformed = ctx.alloc().buffer();
            ....
            ctx.write(transformed, promise);
        } finally {
            content.release();
        }
    } else {
        // Pass non-HttpContent through.
        ctx.write(message, promise);
    }
}

記憶體洩露

因為reference count是netty自身來進行維護的,需要在程式中手動進行release,這樣會帶來一個問題就是記憶體洩露。因為所有的reference都是由程式自己來控制的,而不是由JVM來控制,所以可能因為程式設計師個人的原因導致某些物件reference count無法清零。

為了解決這個問題,預設情況下,netty會選擇1%的buffer allocations樣本來檢測他們是否存在記憶體洩露的情況.

如果發生洩露,則會得到下面的日誌:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

上面提到了一個檢測記憶體洩露的level,netty提供了4種level,分別是:

  • DISABLED---禁用洩露檢測
  • SIMPLE --預設的檢測方式,佔用1% 的buff。
  • ADVANCED - 也是1%的buff進行檢測,不過這個選項會展示更多的洩露資訊。
  • PARANOID - 檢測所有的buff。

具體的檢測選項如下:

java -Dio.netty.leakDetection.level=advanced ...

總結

掌握了netty中的引用計數,就掌握了netty的財富密碼!

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/43-netty-reference-cound/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章