应用在进行数据传输时,往往需要用到缓冲区,常见的缓冲区是JDK NIO类库提供的Buffer,由于JDK提供的Buffer存在一些问题,比如要求定长,读写的时候需要切换等问题,所以Netty提供了自己ByteBuffer的实现,功能更丰富,使用起来也更加方便。
一、创建方式
Netty做为高性能的NIO通信框架,在4.x版本引入了内存池机制,默认采用内存池的方式创建ByteBuf对象,性能有了很大的提升,有效降低了GC的开销,性能上会有很大的提高。
// 传统模式
ByteBuf buf = Unpooled.buffer(10 * 1024);
// 使用内存池
PooledByteBufAllocator allocator = new PooledByteBufAllocator(false);
ByteBuf buf = allocator.heapBuffer(10 * 1024);
内存池就跟线程池的思想类似,只是维护的不是线程,而是一块块的内存区域,Netty的内存池分为PooledArea、PoolChunk和PoolSubpage。PoolArea代表内存中一片连续的区域,由多个PoolChunk组成,而PoolChunk是来组织和管理多个Page,默认16M,每个PoolChunk包括2048个Page,每个Page是8k。这里截取了一张关于PoolChunk内存分配的示例图:
PoolSubpage则是对于小于一个Page的内存处理的,如果存储的数据小于8k,那么Page会被切分成大小相等的多个存储块,具体存储快的大小由第一次申请的内存块大小决定。比如申请的内存块为2k,那么该Page就只能继续分配2k的数据,如果由一个申请了4k,那么就需要在新的Page分配。
二、分配机制
ByteBuffer本质上也是一个Byte数组的缓冲区,可以提供以下几类功能:
1:支持Java基础类型、byte数组、ByteBuffer等的读写
2:缓冲区自身的copy和slice
3:操作位置指针,双指针读写互不干扰
4:容量自动扩容
5:能够灵活切换到其他数据结构,比如JDK的ByteBuffer
不同于JDK ByteBuffer,Netty的ByteBuffer通过两个指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。readerIndex和writerIndex其实位置都是0,随着写入writeIndex会增加,读取数据readerIndex会增加,但是readerIndex始终是不会超过writerIndex,在0~readIndex则是可以被丢弃,readIndex和writeIndex之间的数据则是可以被读取,整体可以类比成一个环,双指针的操作,跟MySQL RedoLog的操作是类似的思想。
由于读写之间是采用不同的指针,因为读写操作不需要调整位置指针,这极大的简化了缓冲区读写操作,避免了对于JDK ByteBuffer的flip()操作导致功能异常。而0~readIndex这部分数据已经被读取,可以调用discardReadBytes()方法释放这部分空间,从而达到复用ByteBuffer,防止ByteBuffer不断扩展的目的。而调用clear()方法则直接重置了读写指针。
三、内存管理
AbstractReferenceCountedByteBuf通过引用计数算法实现对ByteBuffer的管理,跟踪对象的分配和使用,以实现内存的回收、销毁和重利用。
上面是它继承关系图,其中refCnt是引用计数器,勇于对ByeBuf的申请和释放做计数,它的更新通过源自类AtomicIntegerFieldUpdater处理。示例代码如下:
public ByteBuf retain() {
return this.retain0(1);
}
private ByteBuf retain0(int increment) {
int oldRef = refCntUpdater.getAndAdd(this, increment);
if (oldRef > 0 && oldRef + increment >= oldRef) {
return this;
} else {
refCntUpdater.getAndAdd(this, -increment);
throw new IllegalReferenceCountException(oldRef, increment);
}
}
public boolean release() {
return this.release0(1);
}
private boolean release0(int decrement) {
int oldRef = refCntUpdater.getAndAdd(this, -decrement);
if (oldRef == decrement) {
this.deallocate();
return true;
} else if (oldRef >= decrement && oldRef - decrement <= oldRef) {
return false;
} else {
refCntUpdater.getAndAdd(this, decrement);
throw new IllegalReferenceCountException(oldRef, -decrement);
}
}
refCnt使用volatile解决多线程并发之间线程可见的问题,同时也避免了指令重排。所以每调用一次retain()方法,计数器就+1,如果累加后计数器小于0或者发生数组越界,那么则抛出IllegalReferenceCountException异常。调用release()方法释放引用,如果oldRef等于decrement,则说明对象申请和释放次数相同,refCnt为0,ByteBuf已经没有被引用了,那么就执行释放操作。
需要注意的是,不同的ByteBuf模式,deallocate释放的策略不同,如果是内存池模式,则需要将ByteBuf返回到内存池中以便复用。
protected final void deallocate() {
if (this.handle >= 0L) {
long handle = this.handle;
this.handle = -1L;
this.memory = null;
this.tmpNioBuf = null;
this.chunk.arena.free(this.chunk, handle, this.maxLength, this.cache);
this.chunk = null;
this.recycle();
}
}
如果是内存模式,直接设置为空,等待GC回收。
protected void deallocate() {
ByteBuffer buffer = this.buffer;
if (buffer == null) {
return;
}
this.buffer = null;
if (!doNotFree) {
freeDirect(buffer);
}
}
四、需要注意的点
虽然Netty的ByteBuffer给我们提供了,但在使用还是要注意一些点,整理如下:
1:ByteBuffer是非线程安全的,所以申请和释放要防止跨NioEventLoop线程与应用线程并发操作;
2:ByteBuffer需要特别注意Bytebuffer被Netty隐式释放的问题,避免应用要用到的时候被Netty隐式释放掉了;
3:性能问题,通常的获取get()、set()方法并不会有性能问题,但要避免在其中做复杂的操作。