文章目录
- Pre
- 概述
- 前置知识: 名词解释
- writeByte 源码解析
- 实现
- ensureWritable0(minWritableBytes)
- ensureWritable0
- alloc().calculateNewCapacity
- 总结
Pre
Netty Review - 直接内存的应用及源码分析
Netty Review - 底层零拷贝源码解析
Netty Review - ByteBuf内存池源码解析
概述
ByteBuf 扩容机制是指在写入数据时,如果当前容量不足以容纳新增的数据,则需要进行动态扩容,以适应数据量的增长。
下面是ByteBuf 扩容机制的详细阐述:
-
容量检查:
在写入数据之前,会先检查当前可写入的容量是否足够。这通常是通过比较写索引和容量之间的关系来实现的。如果当前可写入容量不足,就需要进行扩容操作。 -
内存分配:
当需要扩容时,会分配一个更大的内存空间来存储数据。这个内存空间的大小通常由扩容策略决定,可以是固定大小的增量,也可以是根据某种规则动态计算的。 -
数据迁移:
在分配更大的内存空间后,原有的数据需要从旧的内存空间复制到新的内存空间中。这个过程涉及数据的复制和移动,但通常只涉及到已经写入的部分数据,而未写入的部分则不需要迁移。 -
索引更新:
扩容完成后,需要更新读写索引和容量信息,以反映新的内存空间状态。通常会更新写索引以指向新的可写入位置,同时更新容量信息以反映新的内存空间大小。 -
内存释放:
如果是使用池化的方式分配内存,则在数据迁移完成后,原有的内存空间可能会被释放回内存池中,以便其他 ByteBuf 实例重复利用。
总的来说,ByteBuf 的扩容机制主要包括容量检查、内存分配、数据迁移、索引更新和内存释放等步骤。这个机制确保了 ByteBuf 在写入数据时能够动态地适应数据量的变化,从而保证了其灵活性和高效性。
前置知识: 名词解释
- minNewCapacity:表用户需要写入的值大小
- threshold:阈值,为Bytebuf内部设定容量的最大值
- maxCapacity:Netty最大能接受的容量大小,一般为int的最大值
writeByte 源码解析
这段代码是 ByteBuf 接口中的一个方法声明,表示向缓冲区中写入一个字节,并将写入位置的索引增加 1。
/**
* 向当前 {@code writerIndex} 处设置指定的字节,并将 {@code writerIndex} 在缓冲区中增加 {@code 1}。
* 指定值的高 24 位将被忽略。
* 如果 {@code this.writableBytes} 小于 {@code 1},则将调用 {@link #ensureWritable(int)},
* 尝试扩展容量以容纳。
*/
public abstract ByteBuf writeByte(int value);
这个方法用于向缓冲区中写入一个字节,参数 value 表示要写入的字节值。如果当前可写入的字节数小于 1(即缓冲区容量不足以容纳新的字节),则会调用 ensureWritable(int)
方法来尝试扩展缓冲区的容量,以确保能够容纳新的字节。
实现
ensureWritable0(minWritableBytes)
实现了 ByteBuf 接口中的 writeByte 方法,用于向缓冲区中写入一个字节。
@Override
public ByteBuf writeByte(int value) {
// 确保缓冲区有足够的可写空间
ensureWritable0(1);
// 将字节写入当前写入位置,并将写入位置后移一位
_setByte(writerIndex++, value);
return this;
}
该方法首先调用 ensureWritable0
方法确保缓冲区有足够的可写空间来容纳一个字节。然后调用 _setByte
方法将指定的字节值写入当前的写入位置,并将写入位置向后移动一个字节的长度。最后返回当前 ByteBuf 实例,以支持链式调用。
ensureWritable0
这段代码实现了 ensureWritable0 方法,用于确保缓冲区有足够的可写空间来容纳指定的字节数。以下是对代码的理解和注释:
final void ensureWritable0(int minWritableBytes) {
// 确保缓冲区是可访问的(未被释放)
ensureAccessible();
// 如果可写字节数大于等于要求的最小可写字节数,则无需扩容,直接返回
if (minWritableBytes <= writableBytes()) {
return;
}
// 检查是否超出最大容量限制
if (checkBounds) {
if (minWritableBytes > maxCapacity - writerIndex) {
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
}
// 将当前容量规范化为2的幂次方,以便进行内存分配
int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
// 调整缓冲区容量为新的容量
capacity(newCapacity);
}
该方法首先确保缓冲区是可访问的,即未被释放。然后检查当前可写字节数是否满足需求,如果不满足,则计算需要扩容的容量。如果启用了边界检查(checkBounds),还会检查是否超出了最大容量限制。最后,根据计算得到的新容量,调用 capacity 方法进行容量调整。
alloc().calculateNewCapacity
这段代码实现了 calculateNewCapacity
方法,用于计算缓冲区扩容时的新容量。
static final int CALCULATE_THRESHOLD = 1048576 * 4; // 4 MiB page
@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));
}
// 计算阈值,即4 MiB页面大小
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
// 如果最小新容量等于阈值,则返回阈值
if (minNewCapacity == threshold) {
return threshold;
}
f// 采用步进4MB的方式完成扩容
// 如果超过阈值,则不是按照两倍增长,而是按照阈值增长
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// 采用64为基数,做倍增的方式完成扩容
// 如果未超过阈值,则按照两倍增长,直到大于等于最小新容量或者达到最大容量
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
return Math.min(newCapacity, maxCapacity);
}
该方法首先检查最小新容量是否为正数或零,并确保不大于最大容量。然后根据阈值进行不同的扩容策略:
- 如果最小新容量超过了阈值,则不是按照两倍增长,而是按照阈值增长;
- 如果未超过阈值,则按照两倍增长,直到大于等于最小新容量或者达到最大容量。
- 最后返回计算得到的新容量。
总结
Netty的ByteBuf需要动态扩容来满足需要, 这种动态扩容机制通过阈值来判断采用不同的扩容策略:
- 如果需要的容量等于门限阈值,则直接使用阈值作为新的缓存区容量。
- 如果需要的容量大于阈值,则采用每次步进4MB的方式进行内存扩张,即将需要扩容值除以4MB后乘以4MB,然后将结果与最大容量进行比较,取其中的较小值作为目标容量。
- 如果需要的容量小于阈值,则采用倍增的方式,以64字节作为基本数值,每次翻倍增长(如64,128,256…),直到倍增后的结果大于或等于所需的容量值。