所有的缓冲区,都是为了平衡了 数据产生方 和 数据消费方 的处理效率差异。
ByteBuf 基础 API
本段内容出自 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中
三种类型的 ByteBuf
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());//false
ByteBuf 的改进
Netty 的 ByteBuf ,使用了外观模式,结合 零拷贝、内存池加速、读写索引 对 JDK 的 ByteBuffer 进行了优化。
ByteBuffer 与 ByteBuf 优缺点
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 中去
}
CompositeByteBuf
内存池
Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,框架自己实现了一套创建、回收堆外内存池的相关功能。
为什么要使用内存池?
操作系统中的 零拷贝
为什么需要零拷贝?
应用程序从磁盘或网卡上获取数据时,为了合规的在不同的缓冲区域操作数据,需要来回拷贝数据,在拷贝的过程中,会消耗一部分时间,还可能会占用 CPU ,进而影响系统的处理效率。为了提高应用的吞吐量,我们应该尽量避免数据拷贝的过程出现。
使用零拷贝方式,不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
网络数据拷贝流程的演变:
传统的数据读入和写出流程:
DMA: direct memory access 直接内存拷贝(不使用 CPU)
mmap(Memory Mapping)内存映射:
减少一次内核到用户态的数据拷贝
sendFile 优化
减少一次上下文切换,优化了内核中 cpu 拷贝读写缓冲区内容,缩短了拷贝时间
Sendfile 系统调用在内核版本 2.4 后,将 Kernel buffer 中对应的数据描述信息(内存地址,偏移量)使用 CPU Copy 到相应的 Socket 缓冲区当中,由于复制内容少,所以占用时间忽略不计。
所以,认为使用该方式的拷贝为零拷贝