跳過導覽

引用計數物件

您知道這項頁面是由 Github Wiki 自動產生嗎?您可以親自 在此處 進行改善!

自 Netty 4 版本以來,特定物件的生命週期由其引用計數進行管理,如此 Netty 便可在不再使用物件(或共用資源)時將其歸還至物件池(或物件配置器)。垃圾回收和參考佇列無法提供有效率的即時不可到達性,而引用計數則提供一個替代機制,代價是不便於使用。

ByteBuf 是最顯著的類型,利用引用計數來改善配置和解除配置效能,本頁面將說明 Netty 如何透過 ByteBuf 執行引用計數。

引用計數基礎

引用計數物件的初始引用計數為 1

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

當您釋放引用計數物件,其引用計數將減少 1。如果引用計數達到 0,引用計數物件將解除配置或歸還至其來源的物件池

assert buf.refCnt() == 1;
// release() returns true only if the reference count becomes 0.
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

游標參考

嘗試存取參考計數為 0 的參考計數物件,將會觸發 IllegalReferenceCountException

assert buf.refCnt() == 0;
try {
  buf.writeLong(0xdeadbeef);
  throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
  // Expected
}

增加參考計數

當參考計數物件尚未被銷毀時,也可以透過 retain() 動作來增加參考計數

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

buf.retain();
assert buf.refCnt() == 2;

boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;

誰會銷毀它?

一般的經驗法則為,最後存取參考計數物件的元件,同時也需負責銷毀該參考計數物件。具體而言

  • 如果 [傳送] 元件打算傳遞一個參考計數物件給另一個 [接收] 元件,傳送元件通常不需銷毀它,而是把這個決定委派給接收元件。
  • 如果一個元件使用了一個參考計數物件,並且知道不會再有其他任何東西會存取它(即不會把參考傳遞給另一個元件),則該元件應該把它銷毀。

以下是個簡單的範例

public ByteBuf a(ByteBuf input) {
    input.writeByte(42);
    return input;
}

public ByteBuf b(ByteBuf input) {
    try {
        output = input.alloc().directBuffer(input.readableBytes() + 1);
        output.writeBytes(input);
        output.writeByte(42);
        return output;
    } finally {
        input.release();
    }
}

public void c(ByteBuf input) {
    System.out.println(input);
    input.release();
}

public void main() {
    ...
    ByteBuf buf = ...;
    // This will print buf to System.out and destroy it.
    c(b(a(buf)));
    assert buf.refCnt() == 0;
}
動作 誰應該釋放? 誰釋放了?
1. main() 建立 buf bufmain()
2. main() 使用 buf 呼叫 a() bufa()
3. a() 僅傳回 buf bufmain()
4. main() 使用 buf 呼叫 b() bufb()
5. b() 傳回 buf 的副本 bufb(), copymain() b() 釋放 buf
6. main() 使用 copy 呼叫 c() copyc()
7. c() 接收 copy copyc() c() 釋放 copy

衍生緩衝區

ByteBuf.duplicate()ByteBuf.slice()ByteBuf.order(ByteOrder) 會建立一個與父緩衝區共用記憶體區域的衍生緩衝區。衍生緩衝區沒有自己的參考計數,而是共用父緩衝區的參考計數。

ByteBuf parent = ctx.alloc().directBuffer();
ByteBuf derived = parent.duplicate();

// Creating a derived buffer does not increase the reference count.
assert parent.refCnt() == 1;
assert derived.refCnt() == 1;

相較之下,ByteBuf.copy()ByteBuf.readBytes(int)不是衍生緩衝區。傳回的 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();
}

ByteBufHolder 介面

有時候,ByteBuf 會包含在一個緩衝區持有者中,例如 DatagramPacketHttpContentWebSocketframe。這些類型會擴充一個名為 ByteBufHolder 的共用介面。

緩衝保留器共用其包含緩衝的參考次數,就像衍生緩衝一樣。

ChannelHandler 中的參考次數

入站訊息

當事件循環將資料讀取至 ByteBuf 並觸發 channelRead() 事件時,對應管線中的 ChannelHandler 負責釋出緩衝。因此,使用接收資料的處理常式應在 channelRead() 處理常式方法中呼叫資料上的 release()

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

如本文檔的「由誰刪除?」部分所述,如果你的處理常式將緩衝(或任何參考次數物件)傳遞給下一個處理常式,你無需釋出。

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

請注意,ByteBuf 並非 Netty 中唯一的參考次數類型。如果你正在處理解碼器產生的訊息,則訊息也很有可能是參考次數。

// Assuming your handler is placed next to `HttpRequestDecoder`
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    if (msg instanceof HttpRequest) {
        HttpRequest req = (HttpRequest) msg;
        ...
    }
    if (msg instanceof HttpContent) {
        HttpContent content = (HttpContent) msg;
        try {
            ...
        } finally {
            content.release();
        }
    }
}

如果感到困惑或想要簡化訊息的釋出,你可以使用 ReferenceCountUtil.release()

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

或者,你可以考慮擴充 SimpleChannelHandler,該 SimpleChannelHandler 會對你收到的所有訊息呼叫 ReferenceCountUtil.release(msg)

出站訊息

不同於入站訊息,出站訊息是由你的應用程式建立,而 Netty 負責在將其寫入網路後釋出。但是,攔截你的寫入請求的處理常式應確保適當釋出任何中間物件。(例如編碼器)

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

// Transformation
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);
    }
}

偵錯緩衝外洩

參考次數計算的缺點是很容易讓參考次數物件外洩。由於 JVM 無法得知 Netty 實作的參考次數計算,因此在這些物件變成不可存取後,JVM 會自動將它們回收,即使它們的參考次數不為零。一旦回收,物件就無法復活,因此無法回存到它的來源池,從而造成記憶體外洩。

幸運的是,儘管難以找到外洩,但 Netty 預設會對大約 1% 的緩衝配置進行取樣,以檢查你的應用程式中是否有外洩。如果發生外洩,你會發現下列記錄訊息

外洩:在 ByteBuf.release() 被垃圾回收之前,並未呼叫它。啟用進階外洩回報以找出外洩位置。若要啟用進階外洩回報,請指定 JVM 選項 '-Dio.netty.leakDetectionLevel=advanced' 或呼叫 ResourceLeakDetector.setLevel()

使用上面提到的 JVM 選項重新啟動你的應用程式,你會看到你的應用程式中最近存取外洩緩衝的位置。下列輸出顯示我們的單元測試 (XmlFrameDecoderTest.testDecodeWithXml()) 中的外洩

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1
#1:
	io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)
	io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)
	io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
	...

Created at:
	io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
	io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
	io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465)
	io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697)
	io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656)
	io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198)
	io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174)
	io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227)
	io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140)
	io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74)
	io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142)
	io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
	io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176)
	io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147)
	io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
	...

如果你使用 Netty 5 或更高版本,會提供額外資訊來協助你找出最近處理外洩緩衝的處理常式。下列範例顯示外洩緩衝是由名為 EchoServerHandler#0 的處理常式處理,然後又被垃圾回收,這表示 EchoServerHandler#0 可能忘記釋出緩衝。

12:05:24.374 [nioEventLoop-1-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 2
#2:
	Hint: 'EchoServerHandler#0' will handle the message from this point.
	io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:329)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:133)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)
#1:
	io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:589)
	io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:208)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)
Created at:
	io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
	io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
	io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:146)
	io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:107)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:123)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)

外洩偵測等級

目前共有 4 個等級的外洩偵測

  • DISABLED - 完全關閉記憶體外洩偵測,建議不要使用。
  • SIMPLE - 針對 1% 的緩衝區判斷是否有記憶體外洩。預設值。
  • ADVANCED - 針對 1% 的緩衝區判斷外洩緩衝區存取的位置。
  • PARANOID - 和 ADVANCED 相同,但會套用在每個緩衝區。對於自動化測試階段很有用。如果建置輸出來有「LEAK: 」,你可以讓建置失敗。

你可以使用 JVM 選項 -Dio.netty.leakDetection.level 指定記憶體外洩偵測層級。

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

注意:這個屬性以前稱為 io.netty.leakDetectionLevel

避免記憶體外洩的最佳實務

  • 你的單元測試和整合測試應在 PARANOID 記憶體外洩偵測層級執行,以及 SIMPLE 層級執行。
  • 在將應用程式推送到整個叢集之前,先在 SIMPLE 層級長時間執行 Canary,以查看是否有記憶體外洩。
  • 如果有記憶體外洩,就再用 ADVANCED 層級執行 Canary 來取得記憶體外洩來源的提示。
  • 不要將有記憶體外洩的應用程式部署到整個叢集。

在單元測試中修正記憶體外洩

在單元測試中忘記釋放緩衝區或訊息是很常見的。這會產生記憶體外洩警告,但並非表示你的應用程式有記憶體外洩。不用使用 try-finally 區塊來封裝單元測試以釋放所有緩衝區,你可以使用 ReferenceCountUtil.releaseLater() 輔助方法。

import static io.netty.util.ReferenceCountUtil.*;

@Test
public void testSomething() throws Exception {
    // ReferenceCountUtil.releaseLater() will keep the reference of buf,
    // and then release it when the test thread is terminated.
    ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
    ...
}

外部連結

如果 JVM 的 GC 機制仍然存在,為何我們需要對 Netty ByteBuf 手動處理參照計數?

Netty 4 中的緩衝區擁有權:緩衝區的生命週期如何管理?

最後檢索於 2024 年 7 月 19 日