netty 引用計數器 ,垃圾回收

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

引用計數基本原理

一個新創建的引用計數對象的初始引用計數是1。

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

當你釋放掉引用計數對象,它的引用次數減1.如果一個對象的引用計數到達0,該對象就會被 釋放或者歸還到創建它的對象池。

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;

懸掛引用

訪問引用計數爲0的引用計數對象會觸發一次IllegalReferenceCountException:

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 }

增加引用計數

只要引用計數對象未被銷燬,就可以通過調用retain()方法來增加引用次數:

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;

誰來銷燬

一般的原則是,最後訪問引用計數對象的部分負責對象的銷燬。更具體地來說:

  • 如果一個[發送]組件要傳遞一個引用計數對象到另一個[接收]組件,發送組件通常不需要 負責去銷燬對象,而是將這個銷燬的任務推延到接收組件
  • 如果一個組件消費了一個引用計數對象,並且不知道誰會再訪問它(例如,不會再將引用 發送到另一個組件),那麼,這個組件負責銷燬工作 這裏有一個簡單的示例:
01 public ByteBuf a(ByteBuf input) {
02  input.writeByte(42);
03  return input;
04 }
05  
06 public ByteBuf b(ByteBuf input) {
07  try {
08  output = input.alloc().directBuffer(input.readableBytes() + 1);
09  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 }

 

動作 誰應該負責釋放? 誰實際釋放?
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)三個方法, 會創建一個子緩衝區,子緩衝區共享父緩衝區的內存區域。子緩衝區沒有自己的引用計數,而是 共享父緩衝區的引用計數。

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;

但是,調用ByteBuf.copy()和ByteBuf.readBytes(int)創建的並不是子緩衝區,返回的 ByteBuf緩衝區是需要被釋放的。 需要注意,父緩衝區和它的子緩衝區共享引用計數,創建子緩衝區並不會增加引用計數。 因此,當你將子緩衝區傳到應用中的其他組件,必須先調用retain()。

01 ByteBuf parent = ctx.alloc().directBuffer(512);
02 parent.writeBytes(...);
03  
04 try {
05  while (parent.isReadable(16)) {
06  ByteBuf derived = parent.readSlice(16);
07  derived.retain();
08  process(derived);
09  }
10 finally {
11  parent.release();
12 }
13 ...
14  
15 public void process(ByteBuf buf) {
16  ...
17  buf.release();
18 }

ByteBufHolder接口

有時候,ByteBuf被緩衝區容器(buffer holder)持有,像DatagramPacket、HttpComponent和WebSocketFrame。 這些類型都繼承自一個通用接口,叫做ByteBufHolder。 緩衝區容器(buffer holder)共享它持有的緩衝區的引用計數,和子緩衝區一樣。

Channel-handler中的引用計數

入口消息

當一個事件循環(event loop)讀取數據並寫入到ByteBuf,在觸發一次channelRead()事件後,應該由對應pipeline的 ChannelHandler負責去釋放緩衝區的內存。因此,消費接收數據的handler應該在它channelRead()方法中調用數據的 release()方法。

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, 那麼你不需要負責去釋放。

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

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

01 / Assuming your handler is placed next to `HttpRequestDecoder`
02 public void channelRead(ChannelHandlerContext ctx, Object msg) {
03  if (msg instanceof HttpRequest) {
04  HttpRequest req = (HttpRequest) msg;
05  ...
06  }
07  if (msg instanceof HttpContent) {
08  HttpContent content = (HttpContent) msg;
09  try {
10  ...
11  finally {
12  content.release();
13  }
14  }
15 }

如果你有疑慮,或者你想簡化釋放消息內存的過程,你可以使用ReferenceCountUtil.release():

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

同樣地,你可以考慮繼承SimpleChannelHandler,它會幫你調用ReferenceCountUtil.release()釋放所有 你接收到的消息內存。

出口消息

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

01 public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
02  System.err.println("Writing: " + message);
03  ctx.write(message, promise);
04 }
05  
06 // Transformation
07 public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
08  if (message instanceof HttpContent) {
09  // Transform HttpContent to ByteBuf.
10  HttpContent content = (HttpContent) message;
11  try {
12  ByteBuf transformed = ctx.alloc().buffer();
13  ....
14  ctx.write(transformed, promise);
15  finally {
16  content.release();
17  }
18  else {
19  // Pass non-HttpContent through.
20  ctx.write(message, promise);
21  }
22 }

內存泄漏

引用計數的缺點是,引用計數對象容易發生泄露。因爲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塊去釋放所有的緩衝區:

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 }

瞭解更多:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章