所有的缓冲区,都是为了平衡了 数据产生方 和 数据消费方 的处理效率差异。
本段内容出自 ByteBuf 类中的 Java Doc
ByteBuf 内存结构:
+-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | | | (CONTENT) | | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity名称解释:
discardable bytes:已经被读取过的数据,一般情况下可以理解为无效区域。readable bytes:未读取数据,readable bytes数据区的数据是满的,都是等待读取的数据。writable bytes:空闲区域,可以往这块区域写数据。capacity:表示当前 ByteBuf 容量。readerIndex:读数据起点指针,当需要读数据时,就以当前指针为起点往后读取数据。writerIndex:写数据起点指针,当需要写数据时,就以当前指针为起点往后写数据。调用 byteBuf.discardReadBytes() 回收已读的空间:
BEFORE discardReadBytes() +-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity AFTER discardReadBytes() +------------------+--------------------------------------+ | readable bytes | writable bytes (got more space) | +------------------+--------------------------------------+ | | | readerIndex (0) <= writerIndex (decreased) <= capacity移动 readable 空间到 bytebuf 的起始位置,bytebuf总容量不变,可写的容量增加。一般不调用该方法来复制缓冲区的内容。
@Override public ByteBuf discardReadBytes() { if (readerIndex == 0) { ensureAccessible(); return this; } if (readerIndex != writerIndex) { setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制readable bytes区域内容到bytebuf的最前面 writerIndex -= readerIndex; adjustMarkers(readerIndex); readerIndex = 0; } else { ensureAccessible(); adjustMarkers(readerIndex); writerIndex = readerIndex = 0; } return this; }调用 byteBuf.clear() 重置 readerIndex 和 writerIndex:
BEFORE clear() +-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity AFTER clear() +---------------------------------------------------------+ | writable bytes (got more space) | +---------------------------------------------------------+ | | 0 = readerIndex = writerIndex <= capacity它不清除缓冲区内容,而只是清除两个指针的值。
派生缓冲区
duplicate() slice() slice(int, int) readSlice(int) retainedDuplicate() retainedSlice() retainedSlice(int, int) readRetainedSlice(int)以上方法可以在已有的一个 byteBuf 上创建一个视图,共享原来 ByteBuf 的数据,但拥有独立的 readerIndex 等标记。 duplicate() 与 copy() 不同,copy() 将复制一个全新的缓冲区。
用例:
//中文在utf-8字符集中共占3字节 ByteBuf copiedBuffer = Unpooled.copiedBuffer("hi word中", Charset.forName("utf-8")); ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(); byteBuf2.writeByte(2); //hasArray()判断使用的是direct buffer还是heap buffer。heap buffer底层使用的是字节数组,所以会返回true System.out.println("copiedBuffer.hasArray=" + copiedBuffer.hasArray()+" byteBuf2.hasArray=" + byteBuf2.hasArray());//copiedBuffer.hasArray()=true byteBuf2.hasArray=false System.out.println("copiedBuffer.capacity="+copiedBuffer.capacity()+" byteBuf2.capacity="+byteBuf2.capacity());//copiedBuffer.capacity=24 byteBuf2.capacity=256 System.out.println(copiedBuffer.getCharSequence(5, 9, Charset.forName("utf-8")));//rd中Heap Buffer(堆缓冲区)
将 ByteBuf 的数据用 byte array 的方式存储在 JVM 的堆空间中。
优点: 可以利用 JVM 提供的 GC 机制,快速的创建、释放对应的 byte array ,并且提供了直接访问内部字节数组的方法。
缺点: 程序向外部写出数据时,需要先从 用户空间(即堆中) 拷贝数据到 内核空间(即直接内存),再拷贝数据到网卡、磁盘等。不清楚的可以看下操作系统数据拷贝的过程
Direct Buffer(直接缓冲区)
使用的是直接内存(JVM参数 -XX:MaxDirectMemorySize=xxM),由操作系统在本地内存进行数据分配。
优点: 由于数据存在于直接内存,避免了从用户空间拷贝数据到内核空间的步骤,可提高系统的数据吞吐量。
缺点:
直接缓冲区不支持通过字节数组的方式来访问数据。使用直接内存时,空间的分配与释放比堆空间更加复杂,所以处理的速度要慢一些。为了解决这个问题,Netty 使用了内存池。以上两者如何选用:
业务消息的编解码中(即各个 handler 中消息的传递),推荐使用 HeapBuf 类型;对于 I/O 通讯线程在读写数据时,推荐使用 DirectBuf 类型,达到零拷贝的效果。
Composite Buffer(复合缓冲区)
Composite Buffer 中将多个 Heap Buffer 或 Direct Buffer 合并为一个逻辑上的 ByteBuf ,形成一个视图,避免了各个 ByteBuf 在组合时的拷贝。
CompositeByteBuf 与 ByteBuf 组合使用:
//中文在utf-8字符集中共占3字节 ByteBuf copiedBuffer = Unpooled.copiedBuffer("hi word中", Charset.forName("utf-8")); ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(); byteBuf2.writeByte(2); CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); System.out.println("init compositeByteBuf.hasArray() " + compositeByteBuf.hasArray());//true compositeByteBuf.addComponents(true, byteBuf2, copiedBuffer); // compositeByteBuf.addComponent(byteBuf2);//两步操作和addComponents效果一样 // compositeByteBuf.addComponent(copiedBuffer); System.out.println(compositeByteBuf.getByte(0));//2 System.out.println("compositeByteBuf.hasArray() " + compositeByteBuf.hasArray());//falseNetty 的 ByteBuf ,使用了外观模式,结合 零拷贝、内存池加速、读写索引 对 JDK 的 ByteBuffer 进行了优化。
JDK 中 ByteBuffer 的缺点:
用作存储数据的 byte array 被 final 修饰,一旦被初始化后就不能修改,没有自动扩容功能,需要开发者自主完成。当存入的数据超过初始化 byte array 的大小时,将抛出 BufferOverflowException 。
内部只使用了一个 position ,在进行读写切换时,需要调用 flip 或 rewind
Netty 中 ByteBuf 的改进:
Heap Buffe 类型的 ByteBuf 底层使用的是数组,在写入数据的方法中,封装了自动扩容的操作。ByteBuf 使用了读写索引,使用时较 ByteBuffer 更加方便。写入数据时的扩容源码:
io.netty.buffer.AbstractByteBuf: @Override public ByteBuf writeByte(int value) { ensureWritable0(1); _setByte(writerIndex++, value); return this; } ... ... final void ensureWritable0(int minWritableBytes) { final int writerIndex = writerIndex(); final int targetCapacity = writerIndex + minWritableBytes; if (targetCapacity <= capacity()) { ensureAccessible(); return; } if (checkBounds && targetCapacity > maxCapacity) { ensureAccessible(); throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the target capacity to the power of 2. final int fastWritable = maxFastWritableBytes(); int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable : alloc().calculateNewCapacity(targetCapacity, maxCapacity);//计算得到新的数组容量 // Adjust to the new capacity. capacity(newCapacity);//拷贝原来的 byte array 内容到新的、容量更大的 byte array 中去 }Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,框架自己实现了一套创建、回收堆外内存池的相关功能。
为什么要使用内存池?
为什么需要零拷贝?
应用程序从磁盘或网卡上获取数据时,为了合规的在不同的缓冲区域操作数据,需要来回拷贝数据,在拷贝的过程中,会消耗一部分时间,还可能会占用 CPU ,进而影响系统的处理效率。为了提高应用的吞吐量,我们应该尽量避免数据拷贝的过程出现。
使用零拷贝方式,不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
传统的数据读入和写出流程: DMA: direct memory access 直接内存拷贝(不使用 CPU)
mmap(Memory Mapping)内存映射: 减少一次内核到用户态的数据拷贝 sendFile 优化 减少一次上下文切换,优化了内核中 cpu 拷贝读写缓冲区内容,缩短了拷贝时间 Sendfile 系统调用在内核版本 2.4 后,将 Kernel buffer 中对应的数据描述信息(内存地址,偏移量)使用 CPU Copy 到相应的 Socket 缓冲区当中,由于复制内容少,所以占用时间忽略不计。 所以,认为使用该方式的拷贝为零拷贝