引用計數物件
自 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 |
buf →main() |
|
2. main() 使用 buf 呼叫 a() |
buf →a() |
|
3. a() 僅傳回 buf 。 |
buf →main() |
|
4. main() 使用 buf 呼叫 b() |
buf →b() |
|
5. b() 傳回 buf 的副本 |
buf →b() , copy →main() |
b() 釋放 buf |
6. main() 使用 copy 呼叫 c() |
copy →c() |
|
7. c() 接收 copy |
copy →c() |
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();
}
有時候,ByteBuf
會包含在一個緩衝區持有者中,例如 DatagramPacket
、HttpContent
和 WebSocketframe
。這些類型會擴充一個名為 ByteBufHolder
的共用介面。
緩衝保留器共用其包含緩衝的參考次數,就像衍生緩衝一樣。
當事件循環將資料讀取至 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));
...
}
外部連結