一、Java中的Buffer对象
我们知道Buffer是NIO底层的重要组件之一,netty沿用了NIO中的Buffer实现,并且进行了更进一步的封装,所以我们先来了解一下NIO中的Buffer。
Buffer中有三个非常重要的属性,源码中的注解是这样描述的:
capacity:表示一个buffer的容量,这个值一旦确定就不会被改变,类似于一位数组,数组长度一旦确认就不会在变。
limit:表示,第一个元素不能被读或写的索引,并且不能大于capacity。
position:是下一个能被读或写的元素索引。position不能大于limit
相关的读写方法,会从position位置开始,并且随着读写的进行,position会自动的递增操作,如果读写请求的索引值超过了limit,此时就会抛出一个BufferUnderflowException异常。如果是通过索引下标去操作buffer,超过limit就会抛出IndexOutOfBoundsException异常。
Buffer中还有一个重要的属性就是mark,mark初始值为-1,mark是存储position的的一个标志属性。当mark方法执行之后,就会被设置成position,并且mark不能大于position。通过调用reset方法,mark归位。position变为mark值。如果position或者limit被调整成比mark还小的值。position就会被归位。如果mark值为-1的时候调用reset方法就会抛出InvalidMarkException异常。
Buffer为了操作这些属性,Buffer定义了一些可操作的方法
clear():这个方法会把limit设置成capacity,position设置成0,这样方便读和put操作
flip():这个方法会把limit设置成position然后把position设置成0,方便读取操作
rewind():这个方法,会把position设置成0,limit保持不变。也就是方便重读。
以上是对Buffer进行的简单分析。
二、ByteBuf
ByteBuf是netty中的重要组件之一,也是netty的核心数据结构。ByteBuf接口继承自RefeenceCounted,采用的是引用计数法,来判断当前的Buffer是否可以回收。RefeenceCounted中有两个方法,一个是retain一个是release,当你引用了buf就需要调用一下retain,不使用的时候调用一下release释放。当refCount为0的时候表示可以进行内存回收。
ByteBuf中维护着两个索引,分别是readerIndex和writeIndex两个索引把buffer分成了三个区域,无效区、可读区、可写区。可读区是实际内容被存储的区域,任何带有read和skip的方法都是操作这个区域,当索引超出这个区域就会抛出IndexOutOfBoundsException异常。可写区,就是数据即将被写入的区域,当超出就会抛出IndexOutOfBoundsException异常,任何带有write的方法都会操作这个区域。无效区就是已经读取的数据区域,当调用discardReadBytes()就会清除掉这个区域的数据,然后把可读区移动到最开始的位置。还有一个clear方法,这个方法就是清楚所有的数据,也就是把writeIndex置0。
回到源码AbstractNioByteChannel内部类NioByteUnsafe的read方法。
ByteBuf byteBuf = allocHandle.allocate(allocator);//是通过Allocator分配得来
三、ByteBufAllocator
ByteBufAllocator allocator = config.getAllocator();//这也是在read方法中分配的
查看配置类中可知,Allocator是通过如下代码来分配的:
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
继续查看接口,发现默认的Allocator是通过工具类来分配:
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ?
"unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})",
allocType);
}
DEFAULT_ALLOCATOR = alloc;
}
根据io.netty.allocator.type系统配置,获取是池化的还是非池化的配置(区别就在于内存使用完之后否是立即回收,立即回收的就是非池化的,循环使用的就是池化的),默认是池化的。
所以read方法中得到的Allocator是PooledByteBufAllocator实例。netty中的ByteBuf对象都是通过这个Allocator来分配。
四、Handle
回到NioByteUnsafe的read方法,我们可以发现,Buffer的分配离不开Handle,那么Handle具体是什么?我们从源码一探究竟。
public final void read() {
final ChannelConfig config = config();
//获取到实际的Allocator
final ByteBufAllocator allocator = config.getAllocator();
//实例化一个handle
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
do {//这里可知buffer的分配入口是在handle中
byteBuf = allocHandle.allocate(allocator);
//
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
close = allocHandle.lastBytesRead() < 0;
}
allocHandle.incMessagesRead(1);
} while (allocHandle.continueReading());
allocHandle.readComplete();
}
}
Handle是RecvByteBufAllocator的内部类,具体的分配是通过RecvByteBufAllocator中的newHandle方法分配。分析可知RecvByteBufAllocator的默认实现是new AdaptiveRecvByteBufAllocator()。这是在config中实现的。所以recvBufAllocHandle()中的newHandle方法就是在AdaptiveRecvByteBufAllocator实现。newHandle()->new HandleImpl(minIndex, maxIndex, initial),三个参数的默认值分别是64、65536、2048,我们先不管这三个参数值具体含义与作用实际分析过程再去了解。所以我们最终得到的是HandleImpl的实现。
allocHandle.allocate(allocator):这是分配的入口,方法的具体实现在MaxMessageHandle中,->alloc.ioBuffer(guess());其中guess()方法获取到的是初始化容量大小。方法实际返回的是HandleImpl中的nextReceiveBufferSize参数。顾名思义就是即将接收的buffer大小,那么这个参数值是怎么定义的呢?继续查看HandleImpl的构造方法。
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
//根据initial获取数组下标值,根据上边的分析他的默认值是2048
index = getSizeTableIndex(initial);
nextReceiveBufferSize = SIZE_TABLE[index];
}
getSizeTableIndex():方法
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;
}
}
}
getSizeTableIndex()方法就是通过二分法查找,找到最接近initial初始值的在SIZE_TABLE中的下标值。SIZE_TABLE中存储的是小于512的都是按照16的倍数存储,超过512的都是按照512的不断乘以2来存储。所以不管初始值设置的是多少都会递归到2的n次幂上来。所以nextReceiveBufferSize 的初始值是2048.
allocHandle.lastBytesRead(doReadBytes(byteBuf)):这个方法会记录实际读取到的数据长度
allocHandle.incMessagesRead(1):数据读取次数加一操作(totalMessages+1)
allocHandle.continueReading():判断当前是否需要继续读取数据操作,其中一个比较就是totalMessages < maxMessagePerRead(maxMessagePerRead的值默认是16),所以当消息读取次数超过了16次就会跳出循环。
allocHandle.readComplete():这个方法的主要目的是计算出nextReceiveBufferSize值,根据之前的读取结果是增加还是减少。
Handle总结:
public interface Handle {
ByteBuf allocate(ByteBufAllocator var1);//分配Buffer
int guess();//返回当前能读取的最大字节数
void reset(ChannelConfig var1);//重置maxMessagePerRead、totalMessages、totalBytesRead值
void incMessagesRead(int var1);//每次读取之后加一
void lastBytesRead(int var1);//记录当前读取的字节数、以及循环读取的总字节数,根据需要更新nextReceiveBufferSize
int lastBytesRead();//返回最新读取的字节数
void attemptedBytesRead(int var1);//保存预期要读取的字节数
int attemptedBytesRead();
boolean continueReading();//判断当前循环是否需要结束
void readComplete();//完成读取,这个方法会根据当前读取到的字节数,跟当前的设置的预期读取长度进行比较要小,那么就缩小当前设置的nextReceiveBufferSize,相反增加,从源码中可知小的时候,index-1,大于的时候index+4,直到65536。
}
Handle可以理解为不断的更新当前channel读取数据的容量,默认的最大容量为65536,也可以理解Allocator分配Buffer的时候的最大容量是通过Handle来分配。
五、分配过程
以上过程都是为了实现Buffer做准备,我们知道ByteBuf的分配是通过Allocator来实现,他的入口方法在Handle中的allocate(ByteBufAllocator var1)–>alloc.ioBuffer(guess()),guess()方法返回的就是需要分配的容量大小。
public ByteBuf ioBuffer(int initialCapacity) {
if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
return directBuffer(initialCapacity);//走直接内存(默认)
}
return heapBuffer(initialCapacity);//走堆内存
}
@Override
public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
if (initialCapacity == 0 && maxCapacity == 0) {
return emptyBuf;
}
validate(initialCapacity, maxCapacity);
return newDirectBuffer(initialCapacity, maxCapacity);//这是一个抽象方法,具体的实现在子类中这里就分为Pooled和Unpooled两种方式(默认是Pooled)
}
以下就是newDirectBuffer方法在PooledByteBufAllocator中的具体实现。
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();//从缓存中取值
PoolArena<ByteBuffer> directArena = cache.directArena;
final ByteBuf buf;
if (directArena != null) {//如果缓存存在
buf = directArena.allocate(cache, initialCapacity, maxCapacity);//从缓存中分配
} else {
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);//否则返回一个非缓存池的buffer
}
return toLeakAwareBuffer(buf);//这里是包装成可以内存泄露感知的buffer,具体是怎么进行内存泄露感知的后续将会单独介绍
}
以上就是一个简单分配过程,实际的内存分配过程非常复杂,需要一点点的去分析。
以上,有任何不对的地方,请指正,敬请谅解。