ByteBuf 是Netty的常用组件之一,下面我们来看看他的内部原理究竟是什么样子?
ByteBuf的主要特性如下:
首先我们思考一个问题,Java JDK自带的NIO Buffer为什么不用呢?有什么优缺点吗?
1. 两种缓冲区的对比?
NIO ByteBuffer(java.nio.ByteBuffer):
- 只有一个位置指针position,切换读写状态时,需手动调用flip()方式或rewind()方式
- 长度固定,一旦分配完成就不能再扩容和收缩
- 当存入对象大于已有容量时,会引发一场。
再来对比一下Netty封装的AbstractByteBuf:(io.netty.buffer.AbstractByteBuf)
- 读写指针分离
- 并可以在指针操作时进行自动扩容
NIO ByteBuffer 的duplicate()方法可以复制对象,复制后的对象与原对象共享缓冲区的内存,其位置指针独立维护,Netty的ByteBuf也采用了这种功能,设计了内存池。
.
内存池是由一定大小和数量的内存块ByteBuf组成的,这些内存块大小默认为16MB
.
当从Channel中读取数据时,无需每次都分配新的ByteBuf,只需从大的内存块中共享一份内存,并初始化其大小和独立维护读/写指针即可。
.
Netty采用对象的引用计数法,需要手动回收,每复制一份ByteBuf或派生出新的ByteBuf,其引用计数都要增加。
2. AbstractByteBuf源码剖析
- 常用的几个变量
// 读索引
int readerIndex;
// 写索引
int writerIndex;
/**
* 标记读索引
* 在解码时,由于消息不完整,无法处理
* 需要将readerIndex复位
* 此时需要先为索引做个标记
*/
private int markedReaderIndex;
// 标记写索引
private int markedWriterIndex;
// 最大容量
private int maxCapacity;
- 然后看两个关键的方法,一个writeBytes,一个readBytes
/**
* netty扩容后的大小必须是2的整数次幂,这与HashMap的扩容方式类似
* @param src
* @param srcIndex the first index of the source
* @param length the number of bytes to transfer
*
* @return
*/
@Override
public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) {
// 确保可写,当容量不足时自动扩容
ensureWritable(length);
// 缓冲区真正的写操作由子类实现,这里用了设计模式:模板模式
setBytes(writerIndex, src, srcIndex, length);
// 调整写索引
writerIndex += length;
return this;
}
- 继续向下看,是如何进行自动扩容的。
@Override
public ByteBuf ensureWritable(int minWritableBytes) {
// 若最小可写为0,则无需扩容
ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
return this;
}
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()) {
// 获取byteBuf的引用计数,
// 如果返回值为零,则说明该对象被销毁,会抛出异常
ensureAccessible();
return;
}
/**
* 判断判断将要写入的字节数是否大于最大可写字节数(maxCapacity-writerIndex)
* 如果大于则直接抛异常,否则继续执行
*/
if (checkBounds && (targetCapacity < 0 || 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();
// 计算自动扩容后的容量,
// 如果已分配容量 >= 要写入字节数,新的容量=写索引+已分配容量
// 否则,需满足最小容量,必须是2的幂数
int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
: alloc().calculateNewCapacity(targetCapacity, maxCapacity);
// Adjust to the new capacity.
// 由子类将容量调整到新的容量值
capacity(newCapacity);
}
- 我们重点看一下io.netty.buffer.AbstractByteBufAllocator 类下的calculateNewCapacity方法:
/**
* 当threshold小于阈值(4MB)时,新的容量(newCapacity)都是以64为计数向左移位计算出来的
* 通过循环,每次移动1位,直到newCapacity >= minNewCapacity
* 如果计算出来的newCapacity大于maxCapacity,则返回maxCapacity
* 否则返回newCapacity
* 当minNewCapacity > 阈值(4MB)时,
* 先计算minNewCapacity/threshold * threshold的大小
* 如果这个值加上一个threshold(4MB) 大于newCapacity
* 则newCapacity的值取maxCapacity
* 否则newCapacity=minNewCapacity/threshold*threshold + threshold
* @param minNewCapacity
* @param maxCapacity
* @return
*/
@Override
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
// 检查minNewCapacity是否大于0
checkPositiveOrZero(minNewCapacity, "minNewCapacity");
if (minNewCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
minNewCapacity, maxCapacity));
}
// 阈值4MB
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
if (minNewCapacity == threshold) {
return threshold;
}
// 当大于4MB时
// If over threshold, do not double but just increase by threshold.
if (minNewCapacity > threshold) {
// 先获取离minNewCapacity最近的4MB的整数倍数,且小于minNewCapacity
// 例如minNewCapacity=7
// newCapacity = (7 / 4) * 4 = 1,所以新的容量扩大1倍
int newCapacity = minNewCapacity / threshold * threshold;
/**
* 此处新的容量值不会倍增,因为4MB以上内存比较大
* 如果继续倍增,则可能带来额外的内存浪费
* 只能在此基础上+4MB,并判断是否大于maxCapacity
* 若大于则返回maxCapacity
* 否则返回newCapacity+threshold
*/
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// Not over threshold. Double up to 4 MiB, starting from 64.
int newCapacity = 64;
/**
* 当小于4MB时,以64为基础倍增
* 64 -》 128 -》256 -》。。。。直到满足最小容量要求,并以此容量值作为新的容量值
*/
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
// 这里保证 newCapacity<= maxCapacity
return Math.min(newCapacity, maxCapacity);
}
- 我们在Netty自带的Junit 测试类(AbstractByteBufTest)下编写一个程序,看看是不是真的这样扩容:
- 测试代码如下:
@Test
public void testByteBufAutoDilatation(){
buffer.writerIndex(buffer.readerIndex());
// 1048576=4MB, 加9999目的是让它扩容
byte[] bytesArray = new byte[1048576 * 4 + 9999];
for (int i = 0; i < bytesArray.length; i++) {
bytesArray[i] = (byte)1;
}
buffer.writeBytes(bytesArray);
}
-
断点调试到扩容发现,newCapacity=1,因此是在4MB的基础上扩大了1倍。
-
至此,writebytes()方法的自动扩容就分析完毕。下面我们来看readBytes()
@Override
public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
// 检测byteBuf是否可读
// 检测其刻度长度是否小于length
checkReadableBytes(length);
// 数据的具体读取由子类实现
getBytes(readerIndex, dst, dstIndex, length);
// 修改读索引
readerIndex += length;
return this;
}
- 下面拿PooledHeapByteBuf类查看:
@Override
public final ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
// 先检查目标数组的存储空间是否足够用,再检查byteBuf的可读内容是否足够
checkDstIndex(index, length, dstIndex, dst.length);
// 将byteBuf的内容读到dst数组中
System.arraycopy(memory, idx(index), dst, dstIndex, length);
return this;
}
- 另一个类PooledDirectByteBuf:
@Override
public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
// 检查
checkDstIndex(index, length, dstIndex, dst.length);
// 将NIO的byteBuffer中的内容获取到dst数组中
_internalNioBuffer(index, length, true).get(dst, dstIndex, length);
return this;
}
final ByteBuffer _internalNioBuffer(int index, int length, boolean duplicate) {
// 根据readIndex获取偏移量offset
index = idx(index);
// 从memory中夫指出一份内存对象,两者共享缓存区,但其位置指针独立维护
ByteBuffer buffer = duplicate ? newInternalNioBuffer(memory) : internalNioBuffer();
// 设置新的ByteBuffer位置及其最大长度
buffer.limit(index + length).position(index);
return buffer;
}
protected final ByteBuffer internalNioBuffer() {
ByteBuffer tmpNioBuf = this.tmpNioBuf;
if (tmpNioBuf == null) {
this.tmpNioBuf = tmpNioBuf = newInternalNioBuffer(memory);
} else {
tmpNioBuf.clear();
}
return tmpNioBuf;
}
至此,ByteBuf的基础容器AbstractByteBuf就分析完毕,其主要有两点功能:
- 读/写索引分离开
- 在写入前能自动扩容
参考书籍:Netty源码剖析与应用