Netty - 内存大小预测器 RecvByteBufAllocator 源码分析 (包含客户端Channel读消息处理)
Netty - 内存大小预测器 RecvByteBufAllocator 源码分析 (包含客户端Channel读消息处理)
前言
我们知道 Netty 中 对消息的处理 都需要申请内存,而这内存默认是 堆外内存 ,为了增加内存的使用率,减少申请内存的不必要的消耗,诞生出了RecvByteBufAllocator( 内存分配大小预测器 ) 。它可以计算预测出下一次申请 的 byteBuf 的容量大小, 弹性伸缩下次分配 byteBuf 的容量大小。
看到这是不是很神奇,居然可以弹性伸缩 byteBuf的容量。 那么我们就来详细分析下,具体是怎样实现的该需求。
在创建 NioServerSocketChannel 或 NioSocketChannel 时, 会创建 一个config 对象
config 对象有三个主要目的:
1. 对外暴露 Channel 中的属性
2. 创建 内存分配大小预测器
3. 内存分配器
其中 内存分配大小预测器 是我们本篇文章要解析的。
public DefaultChannelConfig(Channel channel) {
this(channel, new AdaptiveRecvByteBufAllocator());
}
由 DefaultChannelConfig 的构造方法可知, 它会创建一个 AdaptiveRecvByteBufAllocator ,该对象就是内存分配大小预测器。 那么下面我们就来以该AdaptiveRecvByteBufAllocator 类 为入口进行分析。
1.继承体系
先来看下 AdaptiveRecvByteBufAllocator 的继承体系图.
如图,该继承体系很清晰明了,实现了两个接口 和一个抽象类。
其中 MaxMessagesRecvByteBufAllocator 对 RecvByteBufAllocator 做了方法的增强
DefaultMaxMessagesRecvByteBufAllocator 是个抽象类,实现了部分的方法。
2.RecvByteBufAllocator
首先来看下 第一个接口 RecvByteBufAllocator 的内部方法作用。
public interface RecvByteBufAllocator {
// 创建 Handler 内存大小预测核心对象
Handle newHandle();
// Handler 内存大小预测的核心接口类 核心的方法由该类来提供
@Deprecated
interface Handle {
// 方法作用: 内存分配器 根据预测的内存大小 来创建 byteBuf
ByteBuf allocate(ByteBufAllocator alloc); // 参数 alloc: 内存分配器
// 方法作用:预测内存的大小
int guess();
// 方法作用: 重置 内存大小预测器的属性
void reset(ChannelConfig config);
// 方法作用: 增加消息读取的数量
void incMessagesRead(int numMessages);
// 方法作用: 设置最后一次读取的消息大小
void lastBytesRead(int bytes);
// 方法作用: 获取最后一次读取的消息大小
int lastBytesRead();
// 方法作用: 设置 预测的内存大小
void attemptedBytesRead(int bytes);
// 方法作用: 获取 预测的内存大小
int attemptedBytesRead();
// 方法作用: 判断是否可以继续 读循环
boolean continueReading();
// 方法作用: 本次读循环完毕
void readComplete();
}
// 对Handler接口类 continueReading方法 的增强
@SuppressWarnings("deprecation")
@UnstableApi
interface ExtendedHandle extends Handle {
// 方法作用: 判断是否可以继续 读循环
boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier);
}
// 代理模式 (本篇文章用不到)
class DelegatingHandle implements Handle {
private final Handle delegate;
public DelegatingHandle(Handle delegate) {
this.delegate = checkNotNull(delegate, "delegate");
}
protected final Handle delegate() {
return delegate;
}
// ..... 省略
}
}
从上述RecvByteBufAllocator 接口的代码中可知:
- RecvByteBufAllocator 中仅有一个方法
newHandle()
创建 Handle对象(真正内存预测器核心逻辑对象) - 内部接口Handle ,它是 真正的 内存预测器 的核心逻辑 接口类
- 内部接口ExtendedHandle 继承了Handle, 对Handle 的增强
3.MaxMessagesRecvByteBufAllocator
public interface MaxMessagesRecvByteBufAllocator extends RecvByteBufAllocator {
// 获取每次读循环操作 最大能读取的消息数量 (每到Channel内 读一次数据,称为一个消息)
int maxMessagesPerRead();
// 设置 每次读循环操作 最大能读取的消息数量
MaxMessagesRecvByteBufAllocator maxMessagesPerRead(int maxMessagesPerRead);
}
4.DefaultMaxMessagesRecvByteBufAllocator
public abstract class DefaultMaxMessagesRecvByteBufAllocator implements MaxMessagesRecvByteBufAllocator {
// 每次读循环操作 最大能读取的消息数量
private volatile int maxMessagesPerRead;
// 是否期望要读取更多的消息 默认是 true
private volatile boolean respectMaybeMoreData = true;
public DefaultMaxMessagesRecvByteBufAllocator() {
this(1);
}
// 参数 maxMessagesPerRead: 每次都循环的 可读取的最大消息数量
public DefaultMaxMessagesRecvByteBufAllocator(int maxMessagesPerRead) {
maxMessagesPerRead(maxMessagesPerRead);
}
// 获取每次都循环 读取的最大消息数量
@Override
public int maxMessagesPerRead() {
return maxMessagesPerRead;
}
@Override
public MaxMessagesRecvByteBufAllocator maxMessagesPerRead(int maxMessagesPerRead) {
checkPositive(maxMessagesPerRead, "maxMessagesPerRead");
this.maxMessagesPerRead = maxMessagesPerRead;
return this;
}
// 设置 是否期望要读取更多的消息
public DefaultMaxMessagesRecvByteBufAllocator respectMaybeMoreData(boolean respectMaybeMoreData) {
this.respectMaybeMoreData = respectMaybeMoreData;
return this;
}
// 获取 是否期望要读取更多的消息
public final boolean respectMaybeMoreData() {
return respectMaybeMoreData;
}
// 内存预测器核心类
public abstract class MaxMessageHandle implements ExtendedHandle {
// 所属Channel中的config对象
private ChannelConfig config;
// 每次读循环操作 最大能读取的消息数量 (每到ch内拉一次数据 称为一个消息)
private int maxMessagePerRead;
// 已经读的消息数量
private int totalMessages;
// 已经读的消息 size字节数
private int totalBytesRead;
// 预估下次读的size字节数
private int attemptedBytesRead;
// 最后一次读的size字节数
private int lastBytesRead;
//true
private final boolean respectMaybeMoreData = DefaultMaxMessagesRecvByteBufAllocator.this.respectMaybeMoreData;
// 用来判断是否继续读取的方法
private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
// 判断 预测读取量 == 最后一次读取量(本次读取量)
// true: 说明 预测读取量 和 读取数据量 一致, Channel中可能 仍存剩余数据未读完.
// false: 说明 Channel中得数据读取完毕 或者 Channel 处于close状态
@Override
public boolean get() {
return attemptedBytesRead == lastBytesRead;
}
};
// 重置 内存大小预测器 ,将变量置为默认值
@Override
public void reset(ChannelConfig config) {
this.config = config;
maxMessagePerRead = maxMessagesPerRead();
totalMessages = totalBytesRead = 0;
}
// 根据 内存预测大小 申请 byteBuf内存
@Override
public ByteBuf allocate(ByteBufAllocator alloc) {
return alloc.ioBuffer(guess());
}
// 增加总消息读取量
@Override
public final void incMessagesRead(int amt) {
totalMessages += amt;
}
// 设置 最后一次读取消息size大小 并增加总读取消息size大小
@Override
public void lastBytesRead(int bytes) {
lastBytesRead = bytes;
if (bytes > 0) {
totalBytesRead += bytes;
}
}
// 获取 最后一次读取消息size大小
@Override
public final int lastBytesRead() {
return lastBytesRead;
}
// 是否继续读取
@Override
public boolean continueReading() {
return continueReading(defaultMaybeMoreSupplier);
}
// 是否继续读取
@Override
public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
// 当返回 true 需要 四个条件都为 true:
// 1. config.isAutoRead() 默认为true
// 2. maybeMoreDataSupplier.get() 当Channel中可能存在剩余未读消息时为 true
// 3. totalMessages < maxMessagePerRead 当总读取消息量 < 最大读取消息量时为true
// 4. totalBytesRead > 0
// 1. 服务端 服务端总为 false
// 2. 客户端 true: 客户端正常读取到数据得情况下会成立
// false: 当客户端读取到得数据量 > Integer.Max时
return config.isAutoRead() &&
(!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
totalMessages < maxMessagePerRead &&
totalBytesRead > 0;
}
@Override
public void readComplete() {
}
// 获取 预测内存大小
@Override
public int attemptedBytesRead() {
return attemptedBytesRead;
}
// 设置 预测内存大小
@Override
public void attemptedBytesRead(int bytes) {
attemptedBytesRead = bytes;
}
// 获取 总读取消息size大小
protected final int totalBytesRead() {
return totalBytesRead < 0 ? Integer.MAX_VALUE : totalBytesRead;
}
}
}
上述代码虽然很长,但是每个方法得功能都言简意赅, 重点关注注释上得内容。
5.AdaptiveRecvByteBufAllocator
public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {
static final int DEFAULT_MINIMUM = 64;
// Use an initial value that is bigger than the common MTU of 1500
static final int DEFAULT_INITIAL = 2048;
static final int DEFAULT_MAXIMUM = 65536;
// 索引增量 4
private static final int INDEX_INCREMENT = 4;
// 索引减量 1
private static final int INDEX_DECREMENT = 1;
// size table
private static final int[] SIZE_TABLE;
static {
/**
* 向sizeTable 添加 [16,32,48,64,....,496]
*/
List<Integer> sizeTable = new ArrayList<Integer>();
for (int i = 16; i < 512; i += 16) {
sizeTable.add(i);
}
/**
* 继续向sizeTable 添加 [16,32,48,64,....,496,512,1024,2048,4096,...Integer.Max] 直到int溢出
*/
// Suppress a warning since i becomes negative when an integer overflow happens
for (int i = 512; i > 0; i <<= 1) { // lgtm[java/constant-comparison]
sizeTable.add(i);
}
/**
* SIZE_TABLE = sizeTable [16,32,48,64,....,496,512,1024,2048,4096,...Integer.Max]
*
* 数组赋值
*/
SIZE_TABLE = new int[sizeTable.size()];
for (int i = 0; i < SIZE_TABLE.length; i ++) {
SIZE_TABLE[i] = sizeTable.get(i);
}
}
// 饱汉 单例模式
@Deprecated
public static final AdaptiveRecvByteBufAllocator DEFAULT = new AdaptiveRecvByteBufAllocator();
// 二分查找法
private static int getSizeTableIndex(final int size) {
for (int low = 0, high = SIZE_TABLE.length - 1;;) {
if (high < low) {
return low;
}
if (high == low) {
return high;
}
int mid = low + high >>> 1;
int a = SIZE_TABLE[mid];
int b = SIZE_TABLE[mid + 1];
if (size > b) {
low = mid + 1;
} else if (size < a) {
high = mid - 1;
} else if (size == a) {
return mid;
} else {
return mid + 1;
}
}
}
private final class HandleImpl extends MaxMessageHandle {
private final int minIndex;
private final int maxIndex;
private int index;
// 下一次预分配的容器大小
private int nextReceiveBufferSize;
// 是否缩小 容器大小
private boolean decreaseNow;
// 参数1: 64在 SIZE_TABLE 的下标
// 参数2: 65536 在SIZE_TABLE的下标
// 参数3: 1024
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
// 计算出来 size 1024 在SIZE_TABLE的 下标
index = getSizeTableIndex(initial);
// nextReceiveBufferSize 表示下一次 分配出来的 byteBuf 容量大小
nextReceiveBufferSize = SIZE_TABLE[index];
}
@Override
public void lastBytesRead(int bytes) {
// 条件成立: 说明 读取的数据量和评估的数据量一致。 说明ch内可能还有数据未读取完.. 还需要继续
if (bytes == attemptedBytesRead()) {
// 这个方法想要更新 nextReceiveBufferSize的大小, 因为前面评估的量被读满了,可能意味着ch内有很多数据,需要更大的容器来读
record(bytes);
}
super.lastBytesRead(bytes);
}
@Override
public int guess() {
return nextReceiveBufferSize;
}
// 参数: 本次从ch内 真实读取的数据量
private void record(int actualReadBytes) {
//举个例子:
// 假设 SIZE_TABLE[index] = 512 => SIZE_TABLE[index-1] = 496
// 如果本次读取的数据量 <= 496 ,说明ch的缓冲区 数据不是很多, 可能不需要那么大的 ByteBuf
// 如果第二次的数据量 还是 <=496 ,那么就确定不需要那么大的 ByteBuf了
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
if (decreaseNow) {
// 初始阶段 定义过: 最小 不能 TABLE_SIZE[minIndex]
index = max(index - INDEX_DECREMENT, minIndex);
// 获取相对减小的 BufferSize值 赋值给 nextReceiveBufferSize
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
// 第一次 设置成 true
decreaseNow = true;
}
} // 条件成立: 说明 ByteBuf容器 已经装满了.. 说明ch内还有很多数据,所以这里让Index 右移一位,得到更大的容器
else if (actualReadBytes >= nextReceiveBufferSize) {
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
@Override
public void readComplete() {
record(totalBytesRead());
}
}
private final int minIndex;
private final int maxIndex;
private final int initial;
public AdaptiveRecvByteBufAllocator() {
//64
//2048
//65536
// 赋值 minIndex maxIndex initial
this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
}
//64
//2048
//65536
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
checkPositive(minimum, "minimum");
if (initial < minimum) {
throw new IllegalArgumentException("initial: " + initial);
}
if (maximum < initial) {
throw new IllegalArgumentException("maximum: " + maximum);
}
// sizeTable [16,32,48,64,....,496,512,1024,2048,4096,...Integer.Max]
// 使用二分查找算法 获取 mininum size在数组内的下标
int minIndex = getSizeTableIndex(minimum);
if (SIZE_TABLE[minIndex] < minimum) {
//因为不能小于minimum 所以这里右移index 确保 SIZE_TABLE[minIndex] >= minimum 值
this.minIndex = minIndex + 1;
} else {
this.minIndex = minIndex;
}
// 使用二分查找算法 获取 maximum size在数组内的下标
int maxIndex = getSizeTableIndex(maximum);
//确保 SIZE_TABLE[maxIndex] <= maximum
if (SIZE_TABLE[maxIndex] > maximum) {
// 因为不能超出 maximum值, 所以这里左移 index
this.maxIndex = maxIndex - 1;
} else {
this.maxIndex = maxIndex;
}
// 初始值 1024
this.initial = initial;
}
@SuppressWarnings("deprecation")
@Override
public Handle newHandle() {
// 参数1: 64在 SIZE_TABLE 的下标
// 参数2: 65536 在SIZE_TABLE的下标
// 参数3: 1024
return new HandleImpl(minIndex, maxIndex, initial);
}
@Override
public AdaptiveRecvByteBufAllocator respectMaybeMoreData(boolean respectMaybeMoreData) {
super.respectMaybeMoreData(respectMaybeMoreData);
return this;
}
}
具体的方法功能,注释上解释的很清楚, 下面我们根据具体得内存预测流程,来再次熟悉上面方法得功能。
6.内存预测流程
这里我们以 客户端 Channel 读消息处理为例 ,看下具体是如何进行内存预测的。
废话不多说,直接上代码:
// 客户端NioSocketChannel read()
@Override
public final void read() {
// 获取 客户端Channel 的config对象
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
// 获取 客户端Channel 的 pipeline对象
final ChannelPipeline pipeline = pipeline();
// 获取 内存分配器
final ByteBufAllocator allocator = config.getAllocator();
// 获取 内存大小预测器
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
// 重置 内存大小预测器
allocHandle.reset(config);
// 读取的消息对象引用
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// 按照预测的内存大小 申请 byteBuf内存
byteBuf = allocHandle.allocate(allocator);
// doReadBytes(byteBuf) 从Channel中读取消息
// 设置本次读取消息的size大小
allocHandle.lastBytesRead(doReadBytes(byteBuf));
// 条件成立: 本次读取消息的size <= 0
// 1. size = 0 本次为读到数据
// 2. size < 0 Channel为 close状态,则会返回 -1
if (allocHandle.lastBytesRead() <= 0) {
// 释放byteBuf
byteBuf.release();
// GC help
byteBuf = null;
// 若 lastBytesRead < 0 则说明 Channel为close状态
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
// 总消息的读取量 + 1
allocHandle.incMessagesRead(1);
readPending = false;
// 向 pipeline 传播 读到的消息 (也就是 处理消息 了)
pipeline.fireChannelRead(byteBuf);
// GC help
byteBuf = null;
}
// 判断是否 需要继续从Channel读数据
while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
-
byteBuf = allocHandle.allocate(allocator)
: 通过guess() 预测内存大小 (首次预测大小默认是 1024), 并申请对应大小的内存 -
allocHandle.lastBytesRead(doReadBytes(byteBuf))
: 设置 lastBytesRead 值 为本次从Channel中实际读取到的数据量大小, 此时内部会根据本次实际读取的数据量大小, 调用 record() 方法 来动态调整下一次预测的内存申请的大小值。 -
allocHandle.incMessagesRead(1)
: 已读消息总量 加1 -
allocHandle.continueReading()
: 判断是否继续进行读循环
若要继续读循环,主要根据下 预测内存大小 与 本次读取数据大小来决定:
- 预测的内存大小 与 本次读的数据大小 一致。 说明预测的内存读满了,可能Channel内还有剩余的数据未读。
若不继续都循环,有以下几点原因:
- Channel close状态 关闭了
- 本次读取数据量大小 < 内存预测的大小 ,说明channel 中的数据已经被读取完毕了
- 本次读取数据量大小 超过了 Integer.Max 值, 变为了 负值。
7. 总结
总的来说, 内存大小预测器, 主要就是根据 本次读取的数据量大小 与 内存预测大小 的值来 弹性伸缩下一次内存预测的大小。 并分别根据 这两者的值,来决定 是否需要继续循环从Channel 中读取数据。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)