1. 程式人生 > >《Netty官方文件》引用計數物件

《Netty官方文件》引用計數物件

原文地址     翻譯:Tyrian

從Netty 4起,物件的生命週期由它們的引用計數來管理,因此,一旦物件不再被引用後,Netty 會將它(或它共享的資源)歸還到物件池(或物件分配器)。在垃圾回收和引用佇列不能保證這麼有效、實時的不可達性檢測的情況下,引用計數以犧牲輕微的便利性為代價,提供了 另一種可選的解決方案。 最值得注意的型別是ByteBuf,它正是利用了引用計數來提升記憶體分配和釋放的效能。這一節 將用ByteBuf來講述引用計數在Netty中是如何工作的。

引用計數基本原理

一個新建立的引用計數物件的初始引用計數是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()呼叫a(buf) buf->a()
3. a()直接返回buf buf->main()
4. main()呼叫b(buf) buf->b()
5. b()返回buf的copy buf->b(),copy->main() b()釋放buf
6. main()呼叫c(copy) copy->c()
7. c()釋放copy copy->c() c()釋放copy

子緩衝區(Derived buffers)

呼叫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被緩衝區容器(buffer holder)持有,像DatagramPacket、HttpComponent和WebSocketFrame。 這些型別都繼承自一個通用介面,叫做ByteBufHolder。 緩衝區容器(buffer holder)共享它持有的緩衝區的引用計數,和子緩衝區一樣。

Channel-handler中的引用計數

入口訊息

當一個事件迴圈(event loop)讀取資料並寫入到ByteBuf,在觸發一次channelRead()事件後,應該由對應pipeline的 ChannelHandler負責去釋放緩衝區的記憶體。因此,消費接收資料的handler應該在它channelRead()方法中呼叫資料的 release()方法。

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

在“誰負責銷燬”一節中我們提到,如果你的handler將緩衝區(或者其他任何引用計數物件)傳遞到下一個handler, 那麼你不需要負責去釋放。

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

需要注意的是,ByteBuf並不是Netty中唯一的引用計數型別。如果你在與解碼程式(decoder)生成的訊息打交道,這些訊息一樣可能 是引用計數的。

/ 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,它會幫你呼叫ReferenceCountUtil.release()釋放所有 你接收到的訊息記憶體。

出口訊息

與入口訊息不同的是,出口訊息是在你的應用中建立的,由Netty負責在將訊息傳送出去後釋放掉。但是,如果你 有攔截寫請求的handler程式,則需要保證正確釋放中間物件(例如,編碼程式)。

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就會將它們GC掉,即時此時它們的引用計數並不為0。一旦物件被GC就不能再訪問,也就不能 歸還到緩衝池,所以會導致記憶體洩露。 慶幸的是,儘管發現記憶體洩露很難,但是Netty會對分配的緩衝區的1%進行取樣,來檢查你的應用中是否存在記憶體 洩露。一旦有記憶體洩露,你將會發現如下日誌訊息:

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

重啟程式時加上上面提到的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) … If you use Netty 5 or above, an additional information is provided to help you find which handler handled the leaked buffer lastly. The following example shows that the leaked buffer was handled by the handler whose name is EchoServerHandler#0 and then garbage-collected, which means it is likely that EchoServerHandler#0 forgot to release the buffer:

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.leakDetectionLevel 來指定記憶體洩露檢查等級

java -Dio.netty.leakDetectionLevel=advanced …

避免洩露最佳實踐

  • 指定SIMPLE和PARANOI等級,執行單元測試和整合測試
  • 在將你的應用部署到整個叢集前,儘可能地用足夠長的時間,使用SIMPLE級別去除錯你的程式,來看是否存在記憶體洩露
  • 如果存在記憶體洩露,使用ADVANCED級別去除錯程式,去獲取記憶體洩漏的位置資訊
  • 不要將存在記憶體洩漏的應用部署到整個叢集

在單元測試中修復記憶體洩露

在單元測試中很容易忘記去釋放緩衝區,這就會生成一個記憶體洩漏的警告。但是這並不意味著你的應用中也存在記憶體洩漏。你可以 在單元測試中使用ReferenceCountUtil.releaseLater()工具類方法,來代替try-finally塊去釋放所有的緩衝區:

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

瞭解更多: