【Netty4】netty ByteBuf (二) 引用计数对象(reference counted objects)

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

相关文章:
netty ByteBuf (一)如何创建ByteBuf对象
netty ByteBuf (二) 引用计数对象(reference counted objects)
netty ByteBuf(三)如何释放ByteBuf

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

下文中的测试例子主要是断言为主,elcipse开启断言添加-ea即可
在这里插入图片描述

基本的引用计数

每个对象的初始计数为1

public class Test {
    public static void main(String[] args) {
        ByteBuf buf = Unpooled.buffer(10);
        assert buf.refCnt() == 1;

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

// release() returns true only if the reference count becomes 0.
        boolean destroyed = buf.release();
        assert destroyed;
        assert buf.refCnt() == 0;

注意:只有引用计数变为0时,release()方法才返回true

悬垂(dangling)引用

尝试访问引用计数为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;  
谁来销毁(destroy)

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

  • 如果组件(component)A把一个引用计数对象传给另一个组件B,那么组件A通常不需要销毁对象,而是把决定权交给组件B。
  • 如果一个组件消费了一个引用计数对象并且知道不会再有其他组件访问这个对象,那么这个组件负责销毁它。
  public static ByteBuf a(ByteBuf input) {
        input.writeByte(42);
        return input;
    }

    public static ByteBuf b(ByteBuf input) {
        try {
            ByteBuf output = input.alloc().directBuffer(input.readableBytes() + 1);
            output.writeBytes(input);
            output.writeByte(42);
            return output;
        } finally {
            input.release();
        }
    }

    public static void c(ByteBuf input) {
        System.out.println(input);
        input.release();
    }

    public static void main(String[] args) {
        ByteBuf buf = Unpooled.buffer(10);
        // This will print buf to System.out and destroy it.
        c(b(a(buf)));
        assert buf.refCnt() == 0;
    }
Action谁应该释放?(左侧是资源,右侧是责任方)谁释放了?(为空,表示当前责任方并没有实际释放行为,一般是转移了负责权至其他组件了)
1. main() 创建了buf bufmain()
2. main() 调用a() 并传入 buf bufa()
3. a() 仅仅返回buf bufmain()
4. main() 调用b() 并传入 buf bufb()
5. b() 返回buf的拷贝 bufb(), copymain() b() releases buf
6. main() 调用c() 并传入copy copyc()
7. c() 吞掉copy copyc() c() releases 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, HttpContent,和WebSocketframe。它们都扩展了一个公共接口,ByteBufHolder。

一个buffer holder共享它所持有的引用计数,如同子缓冲一样。

ChannelHandler中的引用计数
Inbound消息(messages)

当一个事件循环(event loop)读入了数据,用读入的数据创建了ByteBuf,并用这个ByteBuf触发了一个channelRead()事件时,那么管道(pipeline)中相应的ChannelHandler就负责释放这个buffer。因此,处理接收到的数据的handler应该在它的channelRead()中调用bufferrelease()

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)对象,这些对象很可能也是引用计数对象:

// Assuming your handler is placed next to `HttpRequestDecoder`
public class  MyHandler extends ChannelInboundHandlerAdapter
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();
        }
    }
}

上面代码例子中,是假定pipeline 链为HttpRequestDecoder,MyHandler,消息先传入HttpRequestDecoder,然后再传入MyHandler,此时,传入MyHandler中的msg已经是一个对象了,后面也需要调用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)。

// Simple-pass through  
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);  
    }  
}  
解决(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)。也就是说默认日志参数比较简单,不方便查找问题,而通过修改JVM 选项 ‘-Dio.netty.leakDetectionLevel=advanced’ 或调用直接在代码中调用API ResourceLeakDetector.setLevel()来修改日志级别,打印详细日志。
下列输出展示了来自单元测试的一个泄露问题(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选项来指定泄漏检测级别。

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

在单元测试中很容易忘记释放缓冲。这会产生一个泄露的警告,但并不是说就肯定存在泄露。你可以使用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));  
    ...  
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值