Netty数据容器ByteBuf和引用计数ReferenceCounted

762 阅读5分钟

ByteBuf

  • 区别于Java Nio里面的ByteBuffer,读写不需要调用flip。
  • 它使用了读索引写索引来分别记录读取和写入的位置,这样可以实现读写分离,避免了JDK的ByteBuffer需要调用flip方法来切换模式的问题。 image.png
  • 它支持堆缓冲区直接缓冲区复合缓冲区三种类型,分别对应于JVM堆内存,操作系统内存和多个缓冲区的组合。不同类型的缓冲区有不同的优缺点,可以根据场景选择合适的类型。
  • 它支持池化(Pooled)非池化(Unpooled) 两种内存分配方式,池化的方式可以减少内存分配和释放的开销,提高性能,但也增加了复杂度。非池化的方式则更简单,但也更耗费资源。
    • netty中的ByteBuf的池化(Pooled)和非池化(Unpooled)是指内存分配的方式。
      • 池化的ByteBuf是从一个预先申请好的内存块中取一块内存,而非池化的ByteBuf是直接通过JDK底层代码申请一块新的内存。池化的ByteBuf可以减少内存分配和释放的开销,提高性能。
    • 池化和非池化的ByteBuf还有其他一些区别,比如:
      • 池化的ByteBuf需要在使用完后调用release()方法释放内存,而非池化的ByteBuf不需要。
      • 池化的ByteBuf可以使用内部的引用计数器来跟踪内存的使用情况,而非池化的ByteBuf没有引用计数器
      • 池化的ByteBuf可以是直接内存或堆内存,而非池化的ByteBuf只能是堆内存。
  • 它提供了丰富的API来操作缓冲区中的数据,例如get/set方法,read/write方法,slice/duplicate/copy方法等。它还支持链式调用和引用计数。
  • 下面是一个使用netty的bytebuf的代码示例:
    //创建一个堆缓冲区
    ByteBuf heapBuf = Unpooled.buffer(16);
    //写入一些字节
    heapBuf.writeBytes("Hello, world!".getBytes());
    //读取一个字节
    byte b = heapBuf.readByte();
    //打印出来
    System.out.println((char) b); //H
    //标记当前的读索引
    heapBuf.markReaderIndex();
    //读取剩余的字节
    byte[] bytes = new byte[heapBuf.readableBytes()];
    heapBuf.readBytes(bytes);
    //打印出来
    System.out.println(new String(bytes)); //ello, world!
    //重置读索引到标记的位置
    heapBuf.resetReaderIndex();
    //再次读取一个字节
    b = heapBuf.readByte();
    //打印出来
    System.out.println((char) b); //e
    

索引相关

  • 读索引(readerIndex):表示下一个要读取的字节的位置,它的初始值为0,每次读取一个字节后,它会自动增加1,直到达到写索引的值。如果试图读取超过写索引的数据,会抛出IndexOutOfBoundsException异常。
  • 写索引(writerIndex):表示下一个要写入的字节的位置,它的初始值为0,每次写入一个字节后,它会自动增加1,直到达到缓冲区的容量。如果试图写入超过容量的数据,会抛出IndexOutOfBoundsException异常。
  • 标记读索引(markedReaderIndex):表示一个临时保存的读索引的位置,它可以通过markReaderIndex方法来设置,也可以通过resetReaderIndex方法来恢复。这样可以方便地回退到之前的读取位置,而不需要重新计算偏移量。
  • 标记写索引(markedWriterIndex):表示一个临时保存的写索引的位置,它可以通过markWriterIndex方法来设置,也可以通过resetWriterIndex方法来恢复。这样可以方便地回退到之前的写入位置,而不需要重新计算偏移量。
  • 最大容量(maxCapacity):表示缓冲区能够容纳的最大字节数,它通常等于缓冲区分配时指定的容量,但也可以动态扩展,只要不超过Integer.MAX_VALUE。
  • 这些索引之间有一些约束和关系,例如:
    • 0 <= 读索引 <= 写索引 <= 最大容量
    • 可读字节数 = 写索引 - 读索引
    • 可写字节数 = 最大容量 - 写索引
  • 下面是一个使用netty的bytebuf的索引的代码示例:
    //创建一个堆缓冲区
    ByteBuf heapBuf = Unpooled.buffer(16);
    //打印初始状态
    System.out.println("Initial state:");
    System.out.println("readerIndex: " + heapBuf.readerIndex()); //0
    System.out.println("writerIndex: " + heapBuf.writerIndex()); //0
    System.out.println("maxCapacity: " + heapBuf.maxCapacity()); //16
    System.out.println("readableBytes: " + heapBuf.readableBytes()); //0
    System.out.println("writableBytes: " + heapBuf.writableBytes()); //16
    //写入一些字节
    heapBuf.writeBytes("Hello, world!".getBytes());
    //打印写入后的状态
    System.out.println("After writing:");
    System.out.println("readerIndex: " + heapBuf.readerIndex()); //0
    System.out.println("writerIndex: " + heapBuf.writerIndex()); //13
    System.out.println("maxCapacity: " + heapBuf.maxCapacity()); //16
    System.out.println("readableBytes: " + heapBuf.readableBytes()); //13
    System.out.println("writableBytes: " + heapBuf.writableBytes()); //3
    //标记当前的读索引和写索引
    heapBuf.markReaderIndex();
    heapBuf.markWriterIndex();
    //读取一个字节
    byte b = heapBuf.readByte();//会移动readerIndex
    //打印读取后的状态
    System.out.println("After reading:");
    System.out.println("readerIndex: " + heapBuf.readerIndex()); //1
    System.out.println("writerIndex: " + heapBuf.writerIndex()); //13
    System.out.println("maxCapacity: " + heapBuf.maxCapacity()); //16
    System.out.println("readableBytes: " + heapBuf.readableBytes()); //12
    System.out.println("writableBytes: " + heapBuf.writableBytes()); //3
    //重置读索引和写索引到标记的位置
    heapBuf.resetReaderIndex();
    heapBuf.resetWriterIndex();
    //打印重置后的状态
    System.out.println("After resetting:");
    System.out.println("readerIndex: " + heapBuf.readerIndex()); //0
    System.out.println("writerIndex: " + heapBuf.writerIndex()); //13
    System.out.println("maxCapacity: " + heapBuf.maxCapacity()); //16
    System.out.println("readableBytes: " + heapBuf.readableBytes()); //13
    System.out.println("writableBytes: " + heapBuf.writableBytes()); //3
    

Netty中零拷贝的概念

  • 零拷贝是一种避免不必要的数据拷贝的技术,可以提高网络编程的性能。Netty中的零拷贝主要体现在以下几个方面:
    • Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。
    • Netty提供了CompositeByteBuf类,实现了ByteBuf的聚合,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。
      // 使用ByteBufAllocator.compositeBuffer()方法
      CompositeByteBuf compositeBuffer = ByteBufAllocator.DEFAULT.compositeBuffer();
      // 使用Unpooled.wrappedBuffer(ByteBuf...)方法
      CompositeByteBuf compositeBuffer = Unpooled.wrappedBuffer(byteBuf1, byteBuf2, byteBuf3);
      // 使用addComponent方法,增加writerIndex,添加到末尾
      compositeBuffer.addComponent(true, byteBuf4);
      // 使用addComponent方法,不增加writerIndex,添加到指定位置
      compositeBuffer.addComponent(false, 2, byteBuf5);
      // 使用addComponents方法,增加writerIndex,添加多个ByteBuf到末尾
      compositeBuffer.addComponents(true, byteBuf6, byteBuf7);
      // 使用addComponents方法,不增加writerIndex,添加多个ByteBuf到指定位置
      compositeBuffer.addComponents(false, 3, byteBuf8, byteBuf9);
      // 获取CompositeByteBuf的容量
      int capacity = compositeBuffer.capacity();
      // 获取CompositeByteBuf的可读字节数
      int readableBytes = compositeBuffer.readableBytes();
      // 读取CompositeByteBuf中的一个字节
      byte b = compositeBuffer.readByte();
      // 写入CompositeByteBuf中的一个字节
      compositeBuffer.writeByte(10);
      // 获取CompositeByteBuf中的一个切片
      ByteBuf slice = compositeBuffer.slice(0, 4);
      // 释放CompositeByteBuf占用的内存
      compositeBuffer.release();
      
      • 使用CompositeByteBuf的各种方法来操作它,就像操作一个普通的ByteBuf一样。
    • Netty提供了FileRegion接口和DefaultFileRegion实现,支持文件传输时的零拷贝。FileRegion可以直接将文件数据从文件系统缓存传输到目标Channel,而不需要经过用户空间。
    • Netty利用了Java NIO中的FileChannel.transferTo和transferFrom方法,实现了文件之间的零拷贝传输。这些方法可以直接将文件数据从一个Channel传输到另一个Channel,而不需要通过用户空间缓冲区。
  • Java NIO中关于Buffer的描述详见。

ReferenceCounted

  • 一个需要显式释放的引用计数对象。 当一个新的ReferenceCounted被实例化时,它的引用计数为1。retain()方法增加引用计数,release()方法减少引用计数。如果引用计数减少到0,对象将被显式地释放,访问已释放的对象通常会导致访问违规。
    // 实现ReferenceCounted接口
    public class MyResource implements ReferenceCounted {
       // 引用计数器
       private final AtomicInteger refCnt = new AtomicInteger(1);
    
       // 获取引用计数
       @Override
       public int refCnt() {
          return refCnt.get();
       }
    
       // 增加引用计数
       @Override
       public ReferenceCounted retain() {
          refCnt.incrementAndGet();
          return this;
       }
    
       // 减少引用计数并释放资源
       @Override
       public boolean release() {
         if (refCnt.decrementAndGet() == 0) {
           System.out.println("Resource released");
           return true;
         }
         return false;
       }
     }
    
     // 使用ReferenceCounted对象
     public class MyHandler {
        private MyResource resource;
    
        // 设置资源并增加引用计数
        public void setResource(MyResource resource) {
            this.resource = resource.retain();
        }
    
        // 关闭时减少引用计数
        public void close() {
           resource.release();
        }
     }
    
  • 如果一个实现了ReferenceCounted的对象是其他实现了ReferenceCounted的对象的容器,当容器的引用计数变为0时,包含的对象也会通过release()方法被释放。
    // 实现ReferenceCounted接口的容器类
    public class MyContainer implements ReferenceCounted {
        // 引用计数器
        private final AtomicInteger refCnt = new AtomicInteger(1);
    
        // 包含的资源列表 MyResource也继承自ReferenceCounted
        private final List<MyResource> resources = new ArrayList<>();
    
         // 添加资源并增加引用计数
        public void addResource(MyResource resource) {
           resources.add(resource.retain());
        }
    
        // 减少引用计数并释放资源和包含的对象
        @Override
        public boolean release() {
          if (refCnt.decrementAndGet() == 0) {
             System.out.println("Container released");
             for (MyResource resource : resources) {
                resource.release();
             }
             return true;
           }
           return false;
        }
     }
    

Netty中ByteBuf是基于ReferenceCounted

image.png

  • JDK提供的原子更新类AtomicIntegerFieldUpdater image.png
  • Netty中的ReferenceCountedByteBuf image.png
    • retain0(): image.png
    • 为什么Netty的引用计数不直接使用Atomic类?
      • 对Netty来说ByteBuf太常用了,可能会有非常多的ByteBuf实例,那就会有非常多的Atomic类,太占内存了,所以就使用了基本数据类型。
      • 而且updater是static的,全局只有一个,都通过它更新计数,就可以省下不少内存了。
    • AtomicIntegerFieldupdater要点总结:
      1. updater更新的必须是int类型变量,不能是其包装类型。 image.png
      2. updater更新的必须是volatile类型变量,确保线程之间共享变量时的立即可见性。 image.png
      3. 变量不能是static的,必须要是实例变量。因为Unsafe.objectFielddOffset()方法不支持静态变量(该操作本质上是通过对象实例的偏移量来直接进行赋值,静态变量不属于任何对象)。
      4. 更新器只能修改它可见范围内的变量,因为更新器是通过反射来得到这个变量,如果变量不可见就会报错。
      5. 如果要更新的变量是包装类型,那么可以使用atomicReferenceFieldUpdater来进行更新。
  • Java 并发之CAS概念以及Atomic**类

ReferenceCounted的作用

  • 引用计数,便于管理它们的生命周期和内存回收,如果计数为0的话。 image.png
  • 使用ReferenceCounted对象时,需要遵循一些规则:
    • 当创建一个新的ReferenceCounted对象时,它的引用计数为1。 当传递一个ReferenceCounted对象给另一个组件时,通常不需要释放它,而是将释放的决定权交给接收方。
    • 当消费一个ReferenceCounted对象并且知道没有其他组件会再访问它时,应该释放它。
    • 如果想复用一个ReferenceCounted对象,可以使用retain()方法来增加它的引用计数,以防止它被释放。
    • 如果试图访问一个已经被释放的ReferenceCounted对象,会抛出IllegalReferenceCountException异常。
    • 更多细节详见-Netty.docs: Reference counted objects
  • Netty提供了内存泄漏检测,就是判断哪些ByteBuf没有合理调用retain和release方法。
    • 你可以通过设置系统属性io.netty.leakDetection.level来配置内存泄漏检测的级别,有四个可选值:DISABLEDSIMPLEADVANCEDPARANOID。级别越高,检测越严格,但也会影响性能。

入站数据的源头

image.png

  • 都是调用pipeline的下一个InBoundHandler的channelRead方法和channelReadComplete方法。