从NIO到netty(14) ByteBuf和ReferenceCounted

56 篇文章 2 订阅

ByteBuf是Netty提供的代替jdk的ByteBuffer的一个容器,首先看一下他的具体用法:

public class ByteBufTest0 {
    public static void main(String[] args) {
        ByteBuf byteBuf = Unpooled.buffer(10);//堆缓冲区
        for(int i=0;i<byteBuf.capacity();i++){
            byteBuf.writeByte(i);
        }
//绝对方式  不会改变readerIndex
        for(int i=0;i<byteBuf.capacity();i++){
            System.out.println(byteBuf.getByte(i));
        }
//相对方式 会改变writerIndex
        for(int i=0;i<byteBuf.capacity();i++){
            System.out.println(byteBuf.readByte());
        }
    }
}

  +-------------------+------------------+------------------+
*      | discardable bytes |  readable bytes  |  writable bytes  |
*      +-------------------+------------------+------------------+
*      |                   |                  |                  |
*      0      <=      readerIndex   <=   writerIndex    <=    capacity

readerIndex 控制读的游标,writerIndex 控制写的游标,capacity是容量。

 

ByteBuf根据堆外内存还是堆内内存可以分成三种类型

Heap Buffer
* 这是最常用的类型,ByteBuf将数据存储在JVM堆空间中,并且将实际的数据存放在byte array中来实现。
* 优点:由于数据是存储在JVM堆中,所以可以实现快速的创建和快速释放,并且提供了直接访问内部字节数组的方法。
* 缺点:每次读写数据必须将数组复制到直接缓冲区,再进行网络传输
*
* Direct Buffer
* 在堆外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为是操作系统直接在本地内存分配数据。
* 优点:在socket进行数据传递时,性能非常好,因为数据直接位于操作系统本地内存中,所以不需要从JVM将数据复制到直接缓冲区,性能很好。
* 缺点:因为DirectBuffer在操作系统内存中,所以分配内存空间和释放比堆要负复杂。所以速度要慢一点
* Netty通过提供内存池来解决这个问题。内存池是提前申请好的内存空间。直接缓冲区并不支持通过字节数组的方式来访问数据。
*
* 重点:对于后端的业务消息编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf
*
* 1.Netty的ByteBuf采用了读写索引分离策略,一个初始化的ByteBuf的readerIndex和writerIndex都为0
* 当读索引和写索引出于同一个位置时,如果我们继续读取,就会抛异常
* 2.对于ByteBuf的任何读写都会分别维护读索引和写索引,如果容量不够会自动陪你过扩,JDK会报错。maxCapticy最大容量默认的限制就是Integer.MAX_VALUE;

 

如果按照池化和非池化可以分成两类型

JDK的ByteBuffer的缺点: 
1、final byte[] bb 这是JDK的ByteBuffer对象中用于存储数据的对象声明,可以看到,其字节数组是被声明为final的,也就是长度是固定不变的,一旦分配好后不能动态扩容与收缩, 
而且当待存储的数据字节很大时就很有可能出现那么就会抛出IndexOutOfBoundException,如果要预防这个异常,那就需要在存储事前完全确定好待存储的字节大小。如果ByteBuffer的空间不足,我们只有一种解决方案: 
创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer中的数据复制过去,这一切都需要开发者自己来手动完成。 
2、ByteBuffer只是用一个position指针来标示位置信息,在进行读写切换时就需要调用flip方法或是rewind方法,使用起来很不方便。
Netty的ByteBuf的优点: 
1、存储字节的数组是动态的,其最大值默认是Integer.MAX_VALUE,这里的动态性是体现在write方法中的,write方法在执行时会判断buffer容量,如果不足则自动扩容。

2、ByteBuf的读写索引是完全分开的,使用起来很方便。

 

------------------------------------------

下面说一下引用计数:

我们知道JVM可以回收失去引用的对象,这里采用的是可达性分析法。但是堆外内存是不受JVM控制的,要回收垃圾,就不能依赖JVM的GC,所以这里就引入了ReferenceCounted,采用引用计数法来进行垃圾回收

当引用计数为0的时候,bytebuf数据会被放回池空间或者回收,这取决于用的是pooled还是unpooled
public interface ReferenceCounted {
    这两个方法是增加引用计数的,上面是+1,下面可以指定
    ReferenceCounted retain();
    ReferenceCounted retain(int increment);

    这两个方法是用于监测用的
    ReferenceCounted touch();
    ReferenceCounted touch(Object hint);
     这两个方法是减少引用计数的,上面是-1,下面可以指定 
    boolean release();
    boolean release(int decrement);
}
AbstractReferenceCounted#retain0
private ReferenceCounted retain0(int increment) {
    for (;;) {
        int refCnt = this.refCnt;
        final int nextCnt = refCnt + increment;

        // Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow.
        if (nextCnt <= increment) {
            throw new IllegalReferenceCountException(refCnt, increment);
        }
        if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
            break;
        }
    }
    return this;
}

可以发现retain0是采用自旋加cas的操作来更新

这里有一个

private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> refCntUpdater =
        AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCounted.class, "refCnt");

这里主要说明下AtomicIntegerFieldUpdater的使用

AtomicIntegerFieldUpdater使用反射 更新某个类的内部的一个int类型的并且是volatitle的变量。 
这里提一下AtomicIntegerFieldUpdater: 
1、更新器更新的必须int类型的变量 ,不能是其包装类型。 
2、更新器更新的必须是volatitle类型变量,确保线程之间共享变量时的立即可见性。 
AtomicIntegerFieldUpdater.newUpdater()方法的实现是AtomicIntegerFieldUpdaterImpl: 
AtomicIntegerFieldUpdaterImpl的构造器会对类型进行验证:

if (field.getType() != int.class)
    throw new IllegalArgumentException("Must be integer type");

if (!Modifier.isVolatile(modifiers))
    throw new IllegalArgumentException("Must be volatile type");

3、变量不能是static的,必须是实例变量,因为Unsafe.objectFieldOffset()方法不支持静态变量(cas操作本质上是通过对象实例的偏移量来直接进行赋值) 
4、更新器只能修改可见范围内的变量,因为更新器是通过反射来得到这个变量,如果变量不可见就会报错。 

这里解释下为什么引用计数没有通过for(;;){

AtmoicInteger

}

来实现?

因为如果用AtmoicInteger来实现,那么每一个bytebuf都要有AtmoicInteger来维护这个引用计数,而

AtomicIntegerFieldUpdater是一个类变量,只有一份。这样变相提高了性能。

---------

如果一个byteBuf的引用计数为0,再次调用写方法就会抛异常:

assert buf.refCnt() == 0;
try {
  buf.writeLong(0xdeadbeef);
  throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
  // Expected
}

关于谁去调用release方法:

通用的规则就是谁在最后接触这个byteBuf就去调用realse方法。详细的:

1.如果一个sending组件把一个bytebuf传递给了一个receiving组件,那么sending组件不需要去调用release方法,receiving组件

需要负责调用release方法

2.如果一个消费组件消费了bytebuf,并且知道没有组件再去引用它,那么久需要调用release方法

下面有个例子

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 = ...;
    // This will print buf to System.out and destroy it.
    c(b(a(buf)));
    assert buf.refCnt() == 0;
}
ActionWho should release?Who released?
1. main() creates bufbufmain() 
2. main() calls a() with bufbufa() 
3. a() returns buf merely.bufmain() 
4. main() calls b() with bufbufb() 
5. b() returns the copy of bufbufb()copymain()b() releases buf
6. main() calls c() with copycopyc() 
7. c() swallows copycopyc()c() releases copy

ByteBuf.duplicate()ByteBuf.slice() and ByteBuf.order(ByteOrder)  ByteBufHolder 这些方法调用产生的ByteBuf和调用的byteBuf共享一个引用计数,而 ByteBuf.copy() and ByteBuf.readBytes(int)产生的ByteBuf和原来的ByteBuf不共用一个引用计数。

因此:

 

ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...);

try {
    while (parent.isReadable(16)) {
        ByteBuf derived = parent.readSlice(16);
        derived.retain();-------1
        process(derived);
    }
} finally {
    parent.release();
}
...

public void process(ByteBuf buf) {
    ...
    buf.release();
}

上面这段代码1这行代码千万不能删,如果删除了就会报错

当事件循环将数据读取到bytebuf中并用它触发channelread()事件时,相应管道中的channelhandler负责释放缓冲区。因此,使用接收数据的处理程序应该对其channelRead()处理程序方法中的数据调用release():

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        ...
    } finally {
        buf.release();
    }
}

当然如果一个bytebuf被传递到下一个handler,那么将不负责调用release方法:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    ...
    ctx.fireChannelRead(buf);
}

如果你自己都搞不清释放那个bytebuf了,可以直接调用

ReferenceCountUtil.release(msg);

SimpleChannelHandler中就已经调用了ReferenceCountUtil.release(msg);

与入站处理器不同的时候,出站处理器的消息是由netty创建的,所以当消息被写入channel之后由netty来负责释放消息。但是,拦截写请求的处理程序应该确保正确地释放任何中间对象。(例如编码器)

 

为了帮助你诊断潜在的(资源泄漏)问题,Netty提供了class ResourceLeakDetector1, 它将对你应用程序的缓冲区分配做大约 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().

Netty 目前定义了 4 种泄漏检测级别,

  • DISABLED - disables leak detection completely. Not recommended.
  • SIMPLE - tells if there is a leak or not for 1% of buffers. Default.
  • ADVANCED - tells where the leaked buffer was accessed for 1% of buffers.
  • PARANOID - Same with ADVANCED except that it's for every single buffer. Useful for automated testing phase. You could fail the build if the build output contains 'LEAK: '.

泄露检测级别可以通过将下面的 Java 系统属性设置为表中的一个值来定义:

java -Dio.netty.leakDetectionLevel=ADVANCED

如果带着该 JVM 选项重新启动你的应用程序,你将看到自己的应用程序最近被泄漏的缓冲 区被访问的位置。下面是一个典型的由单元测试产生的泄漏报告:

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还将提供额外的信息,比如是哪一个handler发生内存泄露了

比如下面的例子:

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)

贴上一篇文章 https://netty.io/wiki/reference-counted-objects.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值