【Netty官方文檔翻譯】引用計數對象(reference counted objects)

知乎有關於引用計數和垃圾回收GC兩種方式的詳細講解html

https://www.zhihu.com/question/21539353java

原文出處:http://netty.io/wiki/reference-counted-objects.htmlobjective-c

 

 

自從Netty 4開始,對象的生命週期由它們的引用計數(reference counts)管理,而不是由垃圾收集器(garbage collector)管理了。ByteBuf是最值得注意的,它使用了引用計數來改進分配內存和釋放內存的性能。promise

 

基本的引用計數緩存

 

每一個對象的初始計數爲1:網絡

 

Java代碼   收藏代碼
  1. ByteBuf buf = ctx.alloc().directBuffer();  
  2. assert buf.refCnt() == 1;  

 

 

當你釋放(release)引用計數對象時,它的引用計數減1.若是引用計數爲0,這個引用計數對象會被釋放(deallocate),並返回對象池。app

 

Java代碼   收藏代碼
  1. assert buf.refCnt() == 1;  
  2. // release() returns true only if the reference count becomes 0.  
  3. boolean destroyed = buf.release();  
  4. assert destroyed;  
  5. assert buf.refCnt() == 0;  

 

 

 懸垂(dangling)引用socket

 

嘗試訪問引用計數爲0的引用計數對象會拋出IllegalReferenceCountException異常:工具

 

Java代碼   收藏代碼
  1. assert buf.refCnt() == 0;  
  2. try {  
  3.   buf.writeLong(0xdeadbeef);  
  4.   throw new Error("should not reach here");  
  5. catch (IllegalReferenceCountExeception e) {  
  6.   // Expected  
  7. }  

 

 

增長引用計數oop

 

可經過retain()操做來增長引用計數,前提是此引用計數對象未被銷燬:

(譯者注:跟未使用ARC的objective-c好像)

 

Java代碼   收藏代碼
  1. ByteBuf buf = ctx.alloc().directBuffer();  
  2. assert buf.refCnt() == 1;  
  3.   
  4. buf.retain();  
  5. assert buf.refCnt() == 2;  
  6.   
  7. boolean destroyed = buf.release();  
  8. assert !destroyed;  
  9. assert buf.refCnt() == 1;  

 

誰來銷燬(destroy)

 

一般的經驗法則是誰最後訪問(access)了引用計數對象,誰就負責銷燬(destruction)它。具體來講是如下兩點:

  • 若是組件(component)A把一個引用計數對象傳給另外一個組件B,那麼組件A一般不須要銷燬對象,而是把決定權交給組件B。
  • 若是一個組件再也不訪問一個引用計數對象了,那麼這個組件負責銷燬它。

 

下面是一個簡單的例子:

 

Java代碼   收藏代碼
  1. public ByteBuf a(ByteBuf input) {  
  2.     input.writeByte(42);  
  3.     return input;  
  4. }  
  5.   
  6. public ByteBuf b(ByteBuf input) {  
  7.     try {  
  8.         output = input.alloc().directBuffer(input.readableBytes() + 1);  
  9.         output.writeBytes(input);  
  10.         output.writeByte(42);  
  11.         return output;  
  12.     } finally {  
  13.         input.release();  
  14.     }  
  15. }  
  16.   
  17. public void c(ByteBuf input) {  
  18.     System.out.println(input);  
  19.     input.release();  
  20. }  
  21.   
  22. public void main() {  
  23.     ...  
  24.     ByteBuf buf = ...;  
  25.     // This will print buf to System.out and destroy it.  
  26.     c(b(a(buf)));  
  27.     assert buf.refCnt() == 0;  
  28. }  

 

 

行爲(Action)                          誰來釋放(Who should release)?   誰釋放了(Who released)?

1. main()建立了buf                    buf→main()

2. buf由main()傳給了a()            buf→a()

3. a()僅僅返回了buf                   buf→main()

4. buf由main()傳給了b()            buf→b()

5. b()返回了buf的拷貝               buf→b(), copy→main()                       b()釋放了buf

6. 拷貝由main()傳給了c()          copy→c()

7. c()消耗(swallow)了拷貝     copy→c()                                           c()釋放了拷貝 

 

子緩衝(Derived buffers)

 

ByteBuf.duplicate(), ByteBuf.slice()和ByteBuf.order(ByteOrder)建立了子緩衝,這些緩存共享了它們的父緩衝(parent buffer)的一部份內存。子緩衝沒有本身的引用計數,而是共享父緩衝的引用計數。

Java代碼   收藏代碼
  1. ByteBuf parent = ctx.alloc().directBuffer();  
  2. ByteBuf derived = parent.duplicate();  
  3.   
  4. // Creating a derived buffer does not increase the reference count.  
  5. assert parent.refCnt() == 1;  
  6. assert derived.refCnt() == 1;  

 

注意父緩衝和它的子緩衝共享一樣的引用計數,當建立子緩衝時並不會增長對象的引用計數。所以,若是你要傳遞(pass)一個子緩衝給你的程序中的其餘組件的話,你得先調用retain()。

 

Java代碼   收藏代碼
  1. ByteBuf parent = ctx.alloc().directBuffer(512);  
  2. parent.writeBytes(...);  
  3.   
  4. try {  
  5.     while (parent.isReadable(16)) {  
  6.         ByteBuf derived = parent.readSlice(16);  
  7.         derived.retain();  
  8.         process(derived);  
  9.     }  
  10. finally {  
  11.     parent.release();  
  12. }  
  13. ...  
  14.   
  15. public void process(ByteBuf buf) {  
  16.     ...  
  17.     buf.release();  
  18. }  

 

ByteBufHolder接口

 

有時候,一個ByteBuf被一個buffer holder持有,諸如DatagramPacket, HttpContent,和WebSocketframe。它們都擴展了一個公共接口,ByteBufHolder。

 

一個buffer holder共享它所持有的引用計數,如同子緩衝同樣。

 

ChannelHandler中的引用計數

 

Inbound消息(messages)

 

當一個事件循環(event loop)讀入了數據,用讀入的數據建立了ByteBuf,並用這個ByteBuf觸發了一個channelRead()事件時,那麼管道(pipeline)中相應的ChannelHandler就負責釋放這個buffer。所以,處理接收到的數據的handler應該在它的channelRead()中調用buffer的release()。

Java代碼   收藏代碼
  1. public void channelRead(ChannelHandlerContext ctx, Object msg) {  
  2.     ByteBuf buf = (ByteBuf) msg;  
  3.     try {  
  4.         ...  
  5.     } finally {  
  6.         buf.release();  
  7.     }  
  8. }  

 

如同在本文檔中的「誰來銷燬」一節所解釋的那樣,若是你的handler傳遞了緩存(或任何引用計數對象)到下一個handler,你就不須要釋放它:

Java代碼   收藏代碼
  1. public void channelRead(ChannelHandlerContext ctx, Object msg) {  
  2.     ByteBuf buf = (ByteBuf) msg;  
  3.     ...  
  4.     ctx.fireChannelRead(buf);  
  5. }  

 

注意ByteBuf不是Netty中惟一一種引用計數對象。由解碼器(decoder)生成的消息(messages)對象,這些對象極可能也是引用計數對象:

Java代碼   收藏代碼
  1. // Assuming your handler is placed next to `HttpRequestDecoder`  
  2. public void channelRead(ChannelHandlerContext ctx, Object msg) {  
  3.     if (msg instanceof HttpRequest) {  
  4.         HttpRequest req = (HttpRequest) msg;  
  5.         ...  
  6.     }  
  7.     if (msg instanceof HttpContent) {  
  8.         HttpContent content = (HttpContent) msg;  
  9.         try {  
  10.             ...  
  11.         } finally {  
  12.             content.release();  
  13.         }  
  14.     }  
  15. }  

 

若是你抱有疑問,或者你想簡化這些釋放消息的工做,你可使用ReferenceCountUtil.release():

Java代碼   收藏代碼
  1. public void channelRead(ChannelHandlerContext ctx, Object msg) {  
  2.     try {  
  3.         ...  
  4.     } finally {  
  5.         ReferenceCountUtil.release(msg);  
  6.     }  
  7. }  

 

還有一種選擇,你能夠考慮繼承SimpleChannelHandler,它在全部接收消息的地方都調用了ReferenceCountUtil.release(msg)。

 

Outbound消息(messages)

 

與inbound消息不一樣,你的程序所建立的消息對象,由Netty負責釋放,釋放的時機是在這些消息被髮送到網絡以後。可是,在發送消息的過程當中,若是有handler截獲(intercept)了你的發送請求,並建立了一些中間對象,則這些handler要確保正確釋放這些中間對象。好比編碼器(encoder)。

 

Java代碼   收藏代碼
  1. // Simple-pass through  
  2. public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {  
  3.     System.err.println("Writing: " + message);  
  4.     ctx.write(message, promise);  
  5. }  
  6.   
  7. // Transformation  
  8. public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {  
  9.     if (message instanceof HttpContent) {  
  10.         // Transform HttpContent to ByteBuf.  
  11.         HttpContent content = (HttpContent) message;  
  12.         try {  
  13.             ByteBuf transformed = ctx.alloc().buffer();  
  14.             ....  
  15.             ctx.write(transformed, promise);  
  16.         } finally {  
  17.             content.release();  
  18.         }  
  19.     } else {  
  20.         // Pass non-HttpContent through.  
  21.         ctx.write(message, promise);  
  22.     }  
  23. }  

 

解決(troubleshooting)buffer泄露

 

引用計數的缺點是容易發生泄露。由於JVM並不知道Netty實現的引用計數的存在,一旦某些對象不可達(unreachable)就會被自動GC掉,即便這些對象的引用計數不爲0。被GC掉的對象就不可用了,所以這些對象也就不能回到對象池中,或者產生內存泄露。

 

 

幸運的是,儘管要找到泄露很困難,但Netty提供了一種方案來幫助發現泄露,此方案默認在你的程序中的已分配的緩衝中取樣(sample)大約1%的緩存,來檢查是否存在泄露。若是存在泄露,你會發現以下日誌:

Plain text代碼   收藏代碼
  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選項(option)從新啓動你的程序,你能夠看到在你的程序中最近訪問已泄露的內存的位置(location)。下列輸出展現了來自單元測試的一個泄露問題(XmlFrameDecoderTest.testDecodeWithXml()):

Java代碼   收藏代碼
  1. Running io.netty.handler.codec.xml.XmlFrameDecoderTest  
  2. 15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.  
  3. Recent access records: 1  
  4. #1:  
  5.     io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)  
  6.     io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)  
  7.     io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)  
  8.     ...  
  9.   
  10. Created at:  
  11.     io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)  
  12.     io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)  
  13.     io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465)  
  14.     io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697)  
  15.     io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656)  
  16.     io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198)  
  17.     io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174)  
  18.     io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227)  
  19.     io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140)  
  20.     io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74)  
  21.     io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142)  
  22.     io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317)  
  23.     io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)  
  24.     io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176)  
  25.     io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147)  
  26.     io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)  
  27.     ...  

 

若是你使用Netty 5或以上的版本,還提供了一個額外的信息,幫助咱們找到最後操做了(handle)泄露緩衝的handler。下面的例子展現了名爲EchoServerHandler#0的handler操做了已泄露的緩衝,而且緩衝已被GC了,這意味着EchoServerHandler#0忘記釋放了這個buffer:

 

Java代碼   收藏代碼
  1. 12:05:24.374 [nioEventLoop-1-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.  
  2. Recent access records: 2  
  3. #2:  
  4.     Hint: 'EchoServerHandler#0' will handle the message from this point.  
  5.     io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:329)  
  6.     io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)  
  7.     io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:133)  
  8.     io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)  
  9.     io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)  
  10.     io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)  
  11.     io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)  
  12.     java.lang.Thread.run(Thread.java:744)  
  13. #1:  
  14.     io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:589)  
  15.     io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:208)  
  16.     io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125)  
  17.     io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)  
  18.     io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)  
  19.     io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)  
  20.     io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)  
  21.     java.lang.Thread.run(Thread.java:744)  
  22. Created at:  
  23.     io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)  
  24.     io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)  
  25.     io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:146)  
  26.     io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:107)  
  27.     io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:123)  
  28.     io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)  
  29.     io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)  
  30.     io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)  
  31.     io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)  
  32.     java.lang.Thread.run(Thread.java:744)  

 

泄露檢測級別

 

當前有4個泄露檢測級別:

  • 禁用(DISABLED)   - 徹底禁止泄露檢測。不推薦。
  • 簡單(SIMPLE)       - 告訴咱們取樣的1%的緩衝是否發生了泄露。默認。
  • 高級(ADVANCED) - 告訴咱們取樣的1%的緩衝發生泄露的地方
  • 偏執(PARANOID)  - 跟高級選項相似,但此選項檢測全部緩衝,而不只僅是取樣的那1%。此選項在自動測試階段頗有用。若是構建(build)輸出包含了LEAK,可認爲構建失敗。

你可使用JVM的-Dio.netty.leakDetectionLevel選項來指定泄漏檢測級別。

Bash代碼   收藏代碼
  1. java -Dio.netty.leakDetectionLevel=advanced ...  

 

避免泄露的最佳實踐

 

  • 在簡單級別和偏執級別上運行你的單元測試和集成測試(integration tests)。
  • 在rolling out到整個集羣以前,使用簡單級別,以一個合理的、足夠長的時間canary(金絲雀?不明因此。。)你的程序,來發現是否存在泄露。
  • 若是存在泄露,再用高級級別來canary以得到一些關於泄露的提示。
  • 不要部署存在泄露的程序到整個集羣。

在單元測試中修復泄露問題

 

在單元測試中很容易忘記釋放緩衝。這會產生一個泄露的警告,但並非說就確定存在泄露。你可使用ReferenceCountUtil.releaseLater()工具方法,放棄用try-finally來包裹你的單元測試代碼以釋放全部的緩衝:

Java代碼   收藏代碼
  1. import static io.netty.util.ReferenceCountUtil.*;  
  2.   
  3. @Test  
  4. public void testSomething() throws Exception {  
  5.     // ReferenceCountUtil.releaseLater() will keep the reference of buf,  
  6.     // and then release it when the test thread is terminated.  
  7.     ByteBuf buf = releaseLater(Unpooled.directBuffer(512));  
  8.     ...  
  9. }  
相關文章
相關標籤/搜索