知乎有關於引用計數和垃圾回收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:網絡
- ByteBuf buf = ctx.alloc().directBuffer();
- assert buf.refCnt() == 1;
當你釋放(release)引用計數對象時,它的引用計數減1.若是引用計數爲0,這個引用計數對象會被釋放(deallocate),並返回對象池。app
- assert buf.refCnt() == 1;
- boolean destroyed = buf.release();
- assert destroyed;
- assert buf.refCnt() == 0;
懸垂(dangling)引用socket
嘗試訪問引用計數爲0的引用計數對象會拋出IllegalReferenceCountException異常:工具
- assert buf.refCnt() == 0;
- try {
- buf.writeLong(0xdeadbeef);
- throw new Error("should not reach here");
- } catch (IllegalReferenceCountExeception e) {
-
- }
增長引用計數oop
可經過retain()操做來增長引用計數,前提是此引用計數對象未被銷燬:
(譯者注:跟未使用ARC的objective-c好像)
- 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;
誰來銷燬(destroy)
一般的經驗法則是誰最後訪問(access)了引用計數對象,誰就負責銷燬(destruction)它。具體來講是如下兩點:
- 若是組件(component)A把一個引用計數對象傳給另外一個組件B,那麼組件A一般不須要銷燬對象,而是把決定權交給組件B。
- 若是一個組件再也不訪問一個引用計數對象了,那麼這個組件負責銷燬它。
下面是一個簡單的例子:
- 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 = ...;
-
- c(b(a(buf)));
- assert buf.refCnt() == 0;
- }
行爲(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)的一部份內存。子緩衝沒有本身的引用計數,而是共享父緩衝的引用計數。
- ByteBuf parent = ctx.alloc().directBuffer();
- ByteBuf derived = parent.duplicate();
-
- assert parent.refCnt() == 1;
- assert derived.refCnt() == 1;
注意父緩衝和它的子緩衝共享一樣的引用計數,當建立子緩衝時並不會增長對象的引用計數。所以,若是你要傳遞(pass)一個子緩衝給你的程序中的其餘組件的話,你得先調用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, HttpContent,和WebSocketframe。它們都擴展了一個公共接口,ByteBufHolder。
一個buffer holder共享它所持有的引用計數,如同子緩衝同樣。
ChannelHandler中的引用計數
Inbound消息(messages)
當一個事件循環(event loop)讀入了數據,用讀入的數據建立了ByteBuf,並用這個ByteBuf觸發了一個channelRead()事件時,那麼管道(pipeline)中相應的ChannelHandler就負責釋放這個buffer。所以,處理接收到的數據的handler應該在它的channelRead()中調用buffer的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)生成的消息(messages)對象,這些對象極可能也是引用計數對象:
- 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(msg)。
Outbound消息(messages)
與inbound消息不一樣,你的程序所建立的消息對象,由Netty負責釋放,釋放的時機是在這些消息被髮送到網絡以後。可是,在發送消息的過程當中,若是有handler截獲(intercept)了你的發送請求,並建立了一些中間對象,則這些handler要確保正確釋放這些中間對象。好比編碼器(encoder)。
- public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
- System.err.println("Writing: " + message);
- ctx.write(message, promise);
- }
-
- public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
- if (message instanceof HttpContent) {
-
- HttpContent content = (HttpContent) message;
- try {
- ByteBuf transformed = ctx.alloc().buffer();
- ....
- ctx.write(transformed, promise);
- } finally {
- content.release();
- }
- } else {
-
- ctx.write(message, promise);
- }
- }
解決(troubleshooting)buffer泄露
引用計數的缺點是容易發生泄露。由於JVM並不知道Netty實現的引用計數的存在,一旦某些對象不可達(unreachable)就會被自動GC掉,即便這些對象的引用計數不爲0。被GC掉的對象就不可用了,所以這些對象也就不能回到對象池中,或者產生內存泄露。
幸運的是,儘管要找到泄露很困難,但Netty提供了一種方案來幫助發現泄露,此方案默認在你的程序中的已分配的緩衝中取樣(sample)大約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()):
- 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或以上的版本,還提供了一個額外的信息,幫助咱們找到最後操做了(handle)泄露緩衝的handler。下面的例子展現了名爲EchoServerHandler#0的handler操做了已泄露的緩衝,而且緩衝已被GC了,這意味着EchoServerHandler#0忘記釋放了這個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) - 跟高級選項相似,但此選項檢測全部緩衝,而不只僅是取樣的那1%。此選項在自動測試階段頗有用。若是構建(build)輸出包含了LEAK,可認爲構建失敗。
你可使用JVM的-Dio.netty.leakDetectionLevel選項來指定泄漏檢測級別。
- java -Dio.netty.leakDetectionLevel=advanced ...
避免泄露的最佳實踐
- 在簡單級別和偏執級別上運行你的單元測試和集成測試(integration tests)。
- 在rolling out到整個集羣以前,使用簡單級別,以一個合理的、足夠長的時間canary(金絲雀?不明因此。。)你的程序,來發現是否存在泄露。
- 若是存在泄露,再用高級級別來canary以得到一些關於泄露的提示。
- 不要部署存在泄露的程序到整個集羣。
在單元測試中修復泄露問題
在單元測試中很容易忘記釋放緩衝。這會產生一個泄露的警告,但並非說就確定存在泄露。你可使用ReferenceCountUtil.releaseLater()工具方法,放棄用try-finally來包裹你的單元測試代碼以釋放全部的緩衝:
- import static io.netty.util.ReferenceCountUtil.*;
-
- @Test
- public void testSomething() throws Exception {
-
-
- ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
- ...
- }