Netty中的ByteBuf
Netty自己的ByteBuf
ByteBuf是为了解决ByteBuffer的问题和满足网络应用程序开发人员的日常需求而设计的.
JDK中ByteBuffer的缺点:
-
无法动态扩容
长度是固定的,不能动态扩展和收缩,当数据大于ByteBuffer容量时,会发生索引越界异常.
-
API使用复杂
读写的时候需要手动调用flip()和rewind()等方法,使用时需要非常谨慎的使用这些api,否则容易出现错误.
ByteBuf做了哪些增强?
- API操作便捷性
- 动态扩容
- 多种ByteBuf实现
- 内存复用机制
- 零拷贝机制
ByteBuf的操作
三个重要属性:
- capacity容量
- readerIndex读取位置
- writerIndex写入位置
提供了两个指针变量来支持顺序读和写操作,分别是readerIndex和writeInDex,也就把缓冲区分成了三个部分:
0[ --已读可丢弃区域-- ]reaerIndex[ --可读区域-- ]writerIndex[ --待写区域-- ]capacity
复制代码
常用方法定义:
- 随机访问索引getByte
- 顺序读read*
- 顺序写write*
- 清除已读内容discardReadBytes
- 清除缓冲区clear
- 搜索操作
- 标记和重置
- 引用计数和释放
我们可以对这些api做一些测试,如下:
package io.netty.example.echo;
import java.util.Arrays;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* @author daniel
* @version 1.0.0
* @date 2021/12/20
*/
public class ApiTest {
public static void main(String[] args) {
//1.创建一个非池化的ByteBuf,大小为10字节
ByteBuf buf = Unpooled.buffer(10);
System.out.println("原始ByteBuf为:" + buf.toString());
System.out.println("1.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//2.写入一段内容
byte[] bytes = {1,2,3,4,5};
buf.writeBytes(bytes);
System.out.println("写入的bytes为:" + Arrays.toString(bytes));
System.out.println("写入一段内容后ByteBuf为:" + buf);
System.out.println("2.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//3.读取一段内容
byte b1 = buf.readByte();
byte b2 = buf.readByte();
System.out.println("读取的bytes为:" + Arrays.toString(new byte[]{b1, b2}));
System.out.println("读取一段内容后ByteBuf为:" + buf);
System.out.println("3.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//4.将读取的内容丢弃
buf.discardReadBytes();
System.out.println("丢弃已读取的内容后ByteBuf为:" + buf);
System.out.println("4.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//5.清空读写指针
buf.clear();
System.out.println("清空读写指针后ByteBuf为:" + buf);
System.out.println("5.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//6.再次写入一段内容,比第一段内容少
byte[] bytes2 = {1,2,3};
buf.writeBytes(bytes2);
System.out.println("再写入的bytes2为:" + Arrays.toString(bytes2));
System.out.println("再写入一段内容后ByteBuf为:" + buf);
System.out.println("6.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//7.将ByteBuf清空
buf.setZero(0, buf.capacity());
System.out.println("内容清空后ByteBuf为:" + buf);
System.out.println("7.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
//8.再次写入一段超过容量的内容
byte[] bytes3 = {1,2,3,4,5,6,7,8,9,10,11};
buf.writeBytes(bytes3);
System.out.println("写入超量的bytes3为:" + Arrays.toString(bytes3));
System.out.println("写入超量内容后ByteBuf为:" + buf);
System.out.println("8.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
System.out.println();
}
}
复制代码
从这些api的使用中就可以体会到ByteBuf比ByteBuffer的强大之处,我们可以深入研究一下它在写入超量数据时的扩容机制,也就是buf.writeBytes(byte[])
方法
ByteBuf动态扩容
容量默认值为256字节,最大值为Integer.MAX_VALUE
,也就是2GB
实际调用AbstractByteBuf.writeBytes
,如下:
AbstractByteBuf.writeBytes
@Override
public ByteBuf writeBytes(byte[] src) {
writeBytes(src, 0, src.length);
return this;
}
复制代码
AbstractByteBuf.writeBytes(src, 0, src.length);
@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
ensureWritable(length); //检查是否有足够的可写空间,是否需要扩容
setBytes(writerIndex, src, srcIndex, length);
writerIndex += length;
return this;
}
复制代码
AbstractByteBuf.ensureWritable(length);
@Override
public ByteBuf ensureWritable(int minWritableBytes) {
ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
return this;
}
复制代码
AbstractByteBuf.ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
final void ensureWritable0(int minWritableBytes) {
final int writerIndex = writerIndex(); //获取当前写下标
final int targetCapacity = writerIndex + minWritableBytes; //计算最少需要的容量
// using non-short-circuit & to reduce branching - this is a hot path and targetCapacity should rarely overflow
if (targetCapacity >= 0 & targetCapacity <= capacity()) { //判断当前容量是否够用
ensureAccessible(); //检查ByteBuf的引用计数,如果为0则不允许继续操作
return;
}
if (checkBounds && (targetCapacity < 0 || targetCapacity > maxCapacity)) { //判断需要的容量是否是合法值,不合法为true直接抛出越界异常
ensureAccessible();//检查ByteBuf的引用计数,如果为0则不允许继续操作
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.(标准化为2的次幂)
final int fastWritable = maxFastWritableBytes();
int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
: alloc().calculateNewCapacity(targetCapacity, maxCapacity); //计算扩容后容量(只要扩容最小64)
// Adjust to the new capacity.
capacity(newCapacity); //设置新的容量
}
复制代码
alloc().calculateNewCapacity(targetCapacity, maxCapacity) -> AbstractByteBufAllocator
@Override
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
checkPositiveOrZero(minNewCapacity, "minNewCapacity"); //最小所需容量
if (minNewCapacity > maxCapacity) { //判断最小所需容量是否合法
throw new IllegalArgumentException(String.format(
"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
minNewCapacity, maxCapacity));
}
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page 阈值超过4M以其他方式计算
if (minNewCapacity == threshold) { //等于4M直接返回4M
return threshold;
}
// If over threshold, do not double but just increase by threshold.
if (minNewCapacity > threshold) { //大于4M,不需要加倍,只需要扩大阈值即可
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// 64 <= newCapacity is a power of 2 <= threshold
final int newCapacity = MathUtil.findNextPositivePowerOfTwo(Math.max(minNewCapacity, 64)); //计算不少于所需容量的最小的2次幂的值
return Math.min(newCapacity, maxCapacity); //取容量所允许的最大值和计算的2次幂的最小值,当然在这儿就是newCapacity=64
}
复制代码
总结一下就是最小所需容量是否等于阈值,如果是直接返回阈值此后直接扩大阈值,否则以64为最小2次幂为基础每次扩大二倍直到阈值.
选择合适的ByteBuf实现
netty针对ByteBuf提供了8中具体的实现方式,如下:
堆内/堆外 | 是否池化 | 访问方式 | 具体实现类 | 备注 |
---|---|---|---|---|
heap堆内 | unpool | safe | UnpooledHeapByteBuf | 数组实现 |
heap堆内 | unpool | unsafe | UnpooledUnsafeHeapByteBuf | Unsafe类直接操作内存 |
heap堆内 | pool | safe | PooledHeapByteBuf | |
heap堆内 | pool | unsafe | PooledUnsafeHeapByteBuf | ~ |
direct堆外 | unpool | safe | UnpooledDirectByteBuf | NIO DirectByteBuffer |
direct堆外 | unpool | unsafe | UnpooleUnsafedDirectByteBuf | ~ |
direct堆外 | pool | safe | PooledDirectByteBuf | ~ |
direct堆外 | pool | unsafe | PooledUnsafeDirectByteBuf | ~ |
在使用时,都是通过ByteBufAllocator分配器进行申请,同时分配器具有内存管理的功能。
在这儿堆内和堆外没有什么区别,对api的使用时一样的,仅仅是通过Unpooled申请的不一样.
那个safe和unsafe有什么区别呢?
以UnpooledHeapByteBuf和UnpooledUnsafeHeapByteBuf中的getByte(int index)方法为例进行分析
UnpooledHeapByteBuf
@Override
public byte getByte(int index) {
ensureAccessible();
return _getByte(index); //真正的获取字节的方法
}
@Override
protected byte _getByte(int index) {
return HeapByteBufUtil.getByte(array, index); //通过HeapByteBufUtil工具类获取数据
}
复制代码
HeapByteBufUtil
static byte getByte(byte[] memory, int index) {
return memory[index];
}
复制代码
UnpooledHeapByteBuf从堆内数组中获取数据,这是安全的
UnpooledUnsafeHeapByteBuf
@Override
public byte getByte(int index) {
checkIndex(index);
return _getByte(index);
}
@Override
protected byte _getByte(int index) {
return UnsafeByteBufUtil.getByte(array, index);
}
复制代码
PlatformDependent0
static byte getByte(byte[] data, int index) {
return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
}
复制代码
UnpooledUnsafeHeapByteBuf是通过UNSAFE来操作内存的
现在我们来研究一下Unsafe
Unsafe的实现
Unsafe意味着不安全的操作,但是更底层的操作会带来性能提升和特殊功能,Netty中会尽力使用unsafe以提升系统性能
Java语言很重要的特性就是一次编译到处运行,所以它针对底层的内存或者其他操作做了很多封装,而unsafe提供了一系列我们操作底层的方法,可能会导致不兼容或不可知的异常.
比如:
- 返回一些低级的内存信息
- addressSize
- pageSize
- 提供用于操作对象及其字段的方法
- allocateInstance
- objectFieldOffset
- 提供用于操作类及其静态字段的方法
- staticFieldOffset
- defineClass
- defineAnonymousClass
- ensureClassInitialized
- 低级的同步原语
- monitorEnter
- tryMonitorEnter
- monitorExit
- compareAndSwapInt
- putOrderedInt
- 直接访问内存的方法
- allocateMomery
- copyMemory
- freeMemory
- getAddress
- getInt
- putInt
- 操作数组
- arrayBaseOffset
- arrayIndexScale
既然这些东西都是jdk封装好的,而是netty也是直接使用的,所以我们无论在使用safe还是unsafe的时候都是无感知的,我们无需关系底层的操作逻辑,因为api都是一样的,只是实现不一样
是否还有一个疑问,池化和非池化是什么意思?
池化和非池化
比如在使用Unpooled.buffer(10)
申请一个缓存区的时候,默认非池化申请的一个缓冲区.
池化和非池化的区别主要是申请内存缓存空间以及缓存空间的使用上,体现为内存复用.
- 在申请内存缓存空间方面:
- pool:池化申请的时候会申请一个比当前所需内存空间更大的内存空间,这就好比一个快递柜,为此netty提供了buf分配管理器专门用来处理这种事情,来创建或复用ByteBuf.
- unpool:非池化申请只会申请特定大小能够使用的内存缓存空间,使用完之后立刻释放,这就像直接把快递放到你的手中,你所在的位置就是开辟的内存空间.
- 在缓存空间使用方面:
- pool:池化申请的内存空间有一定扩容容积,也就是这个快递柜可以存放多个快递,只需要找到对应的方格即可存放,同样buf分配管理器来复用已经创建好的内存空间,在创建ByteBuf的时候已经开辟3中大小的内存块
- normal:16MN
- small:8KB
- tiny:512B
- unpool:毫无疑问,非池化的方式必然是每次都会再去开辟内存空间的.
- pool:池化申请的内存空间有一定扩容容积,也就是这个快递柜可以存放多个快递,只需要找到对应的方格即可存放,同样buf分配管理器来复用已经创建好的内存空间,在创建ByteBuf的时候已经开辟3中大小的内存块
理论如此,netty中是如何做到内存复用的?
在netty中每一个EventLoopThread由PooledBytebufAllocator
内存分配器实力维护了一个线程变量叫做PoolThreadCache
,在这个变量中维护了3种规格的MemoryRegionCache
数组用作内存缓存,MemoryRegionCache
内部是链表,队列里面存Chunk.
首先内存内存分配器会寻找合适的ByteBuf对象进行复用;
之后从内存数组中找到合适的内存空间进行复用;
PoolChunk
里面维护了内存引用,内存复用的做法就是把ByteBuf的memory指向chunk的memory.
如果没有找到对应的缓存空间,则直接向内存申请unpool的缓存空间.
netty中默认(池化)也是这样做的,这也是netty性能高效的一个原因,但是就像example例子一样,如果我们自己创建的话,netty推荐我们使用unpool.
==需要注意的是即使创建了可复用的ByteBuf,但是使用过后一直没有被release,也就是没有被回收也是不能被复用的,这是应用设计时应该注意的.==
说了半天的废话,总算是要说到零拷贝机制了
零拷贝机制
Netty的零拷贝机制是一种应用层的表现,和底层JVM/操作系统内存机制并无过多关联,你可认为netty就是一个软件,我们在用这个软件来创造另一个软件.
-
CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝
什么意思呢?这是一个虚拟的ByteBuf,这个ByteBuf并不是一个,而是一个复合缓冲区,有多个独立的ByteBuf
CompositeByteBuf compositeByteBuf = Unpooled.CompositeBuffer(); ByteBuf byteBuf = compositeByteBuf.addComponents(true,buffer1,buffer2); 复制代码
-
wrapedBuffer()方法,将byte[]数组包装成ByteBuf对象
什么意思呢?这也是一个虚拟的ByteBuf,这个新创建的ByteBuf只是通过memory对此字节数组做了一个引用,避免了复制带来的性能损耗.
ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[]{1,2,3,4,5}); 复制代码
-
slice()方法,将一个ByteBuf对象切分成多个ByteBuf对象
什么意思呢?这还是一个虚拟的ByteBuf,只不过拆分出去的ByteBuf中的memory引用的只是拆分出去的字节位置,并且会以unwarp保留一个对原ByteBuf的引用.
ByteBuf byteBuf = Unpooled.wrappedBuffer("hello".getBytes()); ByteBuf newByteBuf = byteBuf.slice(1,2); 复制代码
ByteBuf的零拷贝机制也是Netty高性能的一个原因.