知乎有关于引用计数和垃圾回收GC两种方式的详细讲解

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

原文出处:http://netty.io/wiki/reference-counted-objects.html

 

自从Netty 4开始,对象的生命周期由它们的引用计数(reference counts)管理,而不是由垃圾收集器(garbage collector)管理了。ByteBuf是最值得注意的,它使用了引用计数来改进分配内存和释放内存的性能。

基本的引用计数

每个对象的初始计数为1:

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

当你释放(release)引用计数对象时,它的引用计数减1.如果引用计数为0,这个引用计数对象会被释放(deallocate),并返回对象池。

  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)引用

尝试访问引用计数为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()操作来增加引用计数,前提是此引用计数对象未被销毁:

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

  1. ByteBuf buf = ctx.alloc().directBuffer();
  2. assert buf.refCnt() == 1;
  3. buf.retain();
  4. assert buf.refCnt() == 2;
  5. boolean destroyed = buf.release();
  6. assert !destroyed;
  7. assert buf.refCnt() == 1;

谁来销毁(destroy)

通常的经验法则是谁最后访问(access)了引用计数对象,谁就负责销毁(destruction)它。具体来说是以下两点:

  • 如果组件(component)A把一个引用计数对象传给另一个组件B,那么组件A通常不需要销毁对象,而是把决定权交给组件B。
  • 如果一个组件不再访问一个引用计数对象了,那么这个组件负责销毁它。

下面是一个简单的例子:

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

行为(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)的一部分内存。子缓冲没有自己的引用计数,而是共享父缓冲的引用计数。

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

注意父缓冲和它的子缓冲共享同样的引用计数,当创建子缓冲时并不会增加对象的引用计数。因此,如果你要传递(pass)一个子缓冲给你的程序中的其他组件的话,你得先调用retain()。

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

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

  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)生成的消息(messages)对象,这些对象很可能也是引用计数对象:

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

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

  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. // Transformation
  7. public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
  8. if (message instanceof HttpContent) {
  9. // 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. }

解决(troubleshooting)buffer泄露

引用计数的缺点是容易发生泄露。因为JVM并不知道Netty实现的引用计数的存在,一旦某些对象不可达(unreachable)就会被自动GC掉,即使这些对象的引用计数不为0。被GC掉的对象就不可用了,因此这些对象也就不能回到对象池中,或者产生内存泄露。

幸运的是,尽管要找到泄露很困难,但Netty提供了一种方案来帮助发现泄露,此方案默认在你的程序中的已分配的缓冲中取样(sample)大约1%的缓存,来检查是否存在泄露。如果存在泄露,你会发现如下日志:

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

  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. Created at:
  10. io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
  11. io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
  12. io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465)
  13. io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697)
  14. io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656)
  15. io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198)
  16. io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174)
  17. io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227)
  18. io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140)
  19. io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74)
  20. io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142)
  21. io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317)
  22. io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
  23. io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176)
  24. io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147)
  25. io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
  26. ...

如果你使用Netty 5或以上的版本,还提供了一个额外的信息,帮助我们找到最后操作了(handle)泄露缓冲的handler。下面的例子展示了名为EchoServerHandler#0的handler操作了已泄露的缓冲,并且缓冲已被GC了,这意味着EchoServerHandler#0忘记释放了这个buffer:

  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选项来指定泄漏检测级别。

  1. java -Dio.netty.leakDetectionLevel=advanced ...

避免泄露的最佳实践

  • 在简单级别和偏执级别上运行你的单元测试和集成测试(integration tests)。
  • 在rolling out到整个集群之前,使用简单级别,以一个合理的、足够长的时间canary(金丝雀?不明所以。。)你的程序,来发现是否存在泄露。
  • 如果存在泄露,再用高级级别来canary以获得一些关于泄露的提示。
  • 不要部署存在泄露的程序到整个集群。

在单元测试中修复泄露问题

在单元测试中很容易忘记释放缓冲。这会产生一个泄露的警告,但并不是说就肯定存在泄露。你可以使用ReferenceCountUtil.releaseLater()工具方法,放弃用try-finally来包裹你的单元测试代码以释放所有的缓冲:

  1. import static io.netty.util.ReferenceCountUtil.*;
  2. @Test
  3. public void testSomething() throws Exception {
  4. // ReferenceCountUtil.releaseLater() will keep the reference of buf,
  5. // and then release it when the test thread is terminated.
  6. ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
  7. ...
  8. }

【Netty官方文档翻译】引用计数对象(reference counted objects)的更多相关文章

  1. netty 引用计数对象(reference counted objects)

    [Netty官方文档翻译]引用计数对象(reference counted objects) http://damacheng009.iteye.com/blog/2013657

  2. Reference counted objects

    Reference counted objects · netty/netty Wiki https://github.com/netty/netty/wiki/Reference-counted-o ...

  3. (20)Cocos2d-x中的引用计数(Reference Count)和自动释放池(AutoReleasePool)

    引用计数 引用计数是c/c++项目中一种古老的内存管理方式.当我8年前在研究一款名叫TCPMP的开源项目的时候,引用计数就已经有了. iOS SDK把这项计数封装到了NSAutoreleasePool ...

  4. Welcome-to-Swift-16自动引用计数(Automatic Reference Counting)

    Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存.大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情.当实例不再被使用时, ...

  5. 2. 引用计数法(Reference Counting)

    1960年,George E. Collins 在论文中发布了引用计数的GC算法. 引用计数法意如了一个概念,那就是"计数器",计数器表示的是对象的人气指数, 也就是有多少程序引用 ...

  6. 从urllib2的内存泄露看python的GC python引用计数 对象的引用数 循环引用

    这里会发现上述代码是存在内存泄露,造成的原因就是lz与ow这两个变量存在循环引用,Python 不知道按照什么样的安全次序来调用对象的 __del__() 函数,导致对象始终存活在 gc.garbag ...

  7. ABP官方文档翻译 2.7 对象到对象的映射

    对象到对象的映射 介绍 IObjectMapper接口 AutoMapper集成 安装 创建映射 自动映射属性 自定义映射 MapTo扩展方法 单元测试 预定义映射 LocalizeableStrin ...

  8. Netty源码分析之ByteBuf引用计数

    引用计数是一种常用的内存管理机制,是指将资源的被引用次数保存起来,当被引用次数变为零时就将其释放的过程.Netty在4.x版本开始使用引用计数机制进行部分对象的管理,其实现思路并不是特别复杂,它主要涉 ...

  9. Andorid Binder进程间通信---Binder本地对象,实体对象,引用对象,代理对象的引用计数

    本文參考<Android系统源码情景分析>,作者罗升阳. 一.Binder库(libbinder)代码: ~/Android/frameworks/base/libs/binder --- ...

随机推荐

  1. Java考试题之四

    QUESTION 73 Given: 10: public class Hello { 11: String title; 12: int value; 13: public Hello() { 14 ...

  2. java的序列化流和打印流

    对象操作流(序列化流) 每次读取和写出的都是JavaBean对象. 序列化:将对象写入到文件中的过程 反序列化:从文件中读取对象到程序的过程 transient: 标识瞬态,序列化的时候,该修饰符修饰 ...

  3. 【codeforces gym】Increasing Costs

    Portal --> Increasing Costs Description 给你一个\(n\)个点无重边无自环的无向连通图,每条边有一个边权,对于每一条边,询问去掉这条边之后有多少个点到\( ...

  4. 【线段树】【P4198】 楼房重建

    Description 小A在平面上(0,0)点的位置,第i栋楼房可以用一条连接(i,0)和(i,Hi)的线段表示,其中Hi为第i栋楼房的高度.如果这栋楼房上任何一个高度大于0的点与(0,0)的连线没 ...

  5. HDU--4768

    题目: Flyer 原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=4768 分析:二分.只需要注意到最多只有一个为奇数,则可以首先求出学生获得的总的传单 ...

  6. 使用 mysql-proxy 监听 mysql 查询

    什么是 mysql-proxy? mysql-proxy是mysql官方提供的mysql中间件服务,上游可接入若干个mysql-client,后端可连接若干个mysql-server. 它使用mysq ...

  7. 利用VisualStudio单元测试框架举一个简单的单元测试例子

    本随笔很简单,不涉及mock和stub对象,而是只给出一个简单的利用Visual Studio单元测试框架的最简单例子.如果需要深入理解Unit Test的原理与艺术,请参考<The art o ...

  8. Linux Wget 命令实例讲解

    Linux wget是一个下载文件的工具,它用在命令行下.对于Linux用户是必不可少的工具,尤其对于网络管理员,经常要下载一些软件或从远程服务器恢复备份到本地服务器.如果我们使用虚拟主机,处理这样的 ...

  9. vue项目post请求405报错解决办法。

    步骤一: 确定ajax语法没有错误. 步骤二: 与后台对接确认请求是否打到nginx上? 步骤三: 检查nginx是否配置了事件转发,比如我们的接口是在,当前地址的8100端口上,并且接口地址上有v1 ...

  10. Shell记录-Shell脚本基础(五)

    Linux中的ps命令是Process Status的缩写.ps命令用来列出系统中当前运行的那些进程.ps命令列出的是当前那些进程的快照,就是执行ps命令的那个时刻的那些进程,如果想要动态的显示进程信 ...