ByteBuf
- 区别于Java Nio里面的ByteBuffer,读写不需要调用flip。
- 它使用了读索引和写索引来分别记录读取和写入的位置,这样可以实现读写分离,避免了JDK的ByteBuffer需要调用flip方法来切换模式的问题。
- 它支持堆缓冲区,直接缓冲区和复合缓冲区三种类型,分别对应于JVM堆内存,操作系统内存和多个缓冲区的组合。不同类型的缓冲区有不同的优缺点,可以根据场景选择合适的类型。
- 它支持池化(Pooled) 和 非池化(Unpooled) 两种内存分配方式,池化的方式可以减少内存分配和释放的开销,提高性能,但也增加了复杂度。非池化的方式则更简单,但也更耗费资源。
- netty中的ByteBuf的池化(Pooled)和非池化(Unpooled)是指内存分配的方式。
- 池化的ByteBuf是从一个预先申请好的内存块中取一块内存,而非池化的ByteBuf是直接通过JDK底层代码申请一块新的内存。池化的ByteBuf可以减少内存分配和释放的开销,提高性能。
- 池化和非池化的ByteBuf还有其他一些区别,比如:
- 池化的ByteBuf需要在使用完后调用release()方法释放内存,而非池化的ByteBuf不需要。
- 池化的ByteBuf可以使用内部的引用计数器来跟踪内存的使用情况,而非池化的ByteBuf没有引用计数器。
- 池化的ByteBuf可以是直接内存或堆内存,而非池化的ByteBuf只能是堆内存。
- netty中的ByteBuf的池化(Pooled)和非池化(Unpooled)是指内存分配的方式。
- 它提供了丰富的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
- JDK提供的原子更新类AtomicIntegerFieldUpdater
- Netty中的ReferenceCountedByteBuf
- retain0():
- 为什么Netty的引用计数不直接使用Atomic类?
- 对Netty来说ByteBuf太常用了,可能会有非常多的ByteBuf实例,那就会有非常多的Atomic类,太占内存了,所以就使用了基本数据类型。
- 而且updater是static的,全局只有一个,都通过它更新计数,就可以省下不少内存了。
- AtomicIntegerFieldupdater要点总结:
- updater更新的必须是int类型变量,不能是其包装类型。
- updater更新的必须是volatile类型变量,确保线程之间共享变量时的立即可见性。
- 变量不能是static的,必须要是实例变量。因为Unsafe.objectFielddOffset()方法不支持静态变量(该操作本质上是通过对象实例的偏移量来直接进行赋值,静态变量不属于任何对象)。
- 更新器只能修改它可见范围内的变量,因为更新器是通过反射来得到这个变量,如果变量不可见就会报错。
- 如果要更新的变量是包装类型,那么可以使用atomicReferenceFieldUpdater来进行更新。
- updater更新的必须是int类型变量,不能是其包装类型。
- retain0():
- Java 并发之CAS概念以及Atomic**类
ReferenceCounted的作用
- 引用计数,便于管理它们的生命周期和内存回收,如果计数为0的话。
- 使用ReferenceCounted对象时,需要遵循一些规则:
- 当创建一个新的ReferenceCounted对象时,它的引用计数为1。 当传递一个ReferenceCounted对象给另一个组件时,通常不需要释放它,而是将释放的决定权交给接收方。
- 当消费一个ReferenceCounted对象并且知道没有其他组件会再访问它时,应该释放它。
- 如果想复用一个ReferenceCounted对象,可以使用retain()方法来增加它的引用计数,以防止它被释放。
- 如果试图访问一个已经被释放的ReferenceCounted对象,会抛出IllegalReferenceCountException异常。
- 更多细节详见-Netty.docs: Reference counted objects
- Netty提供了内存泄漏检测,就是判断哪些ByteBuf没有合理调用retain和release方法。
- 你可以通过设置系统属性
io.netty.leakDetection.level来配置内存泄漏检测的级别,有四个可选值:DISABLED,SIMPLE,ADVANCED和PARANOID。级别越高,检测越严格,但也会影响性能。
- 你可以通过设置系统属性
入站数据的源头
- 都是调用pipeline的下一个InBoundHandler的channelRead方法和channelReadComplete方法。