前言
在java nio包中使用Buffer作为数据存放的载体,分为HeapBuffer与DirectBuffer。netty针对nio中的Buffer缺点和自身的使用特点实现自己的Buffer体系。
Netty的Buffer有如下特点:
- 支持动态扩容
- 读写双指针
- 多种类型HeapBuffer、DirectBuffer、CompositeByteBuf
- 支持引用计数
- 支持池化
Netty Buffer
1. 支持动态扩容
nio ByteBuffer
final byte[] hb;
nio中的Buffer是不支持动态扩容的,当你指定Buffer大小之后,后续就不能修改它的容量。比如ByteBuffer中存放数据的byte数组是由final修饰,final修饰的引用是不可变的。
UnpooledHeapByteBuf
//非 final修饰
byte[] array;
而netty中的支持动态扩容,比如UnpooledHeapByteBuf中存放的byte数组并不是和nio中一样用final修饰,当调用write类方法时候,首先检测是否超出数组容量,如果超出之后将分配一个是原来容量2倍的数组,然后将原数组中的数据拷贝到新数组中,整个过程类似ArrayList的扩容。
2. 读写双指针
nio中的Buffer使用position代表下一个读或者写的位置,所以当每次往Buffer中写完数据之后都需要调用一下flip方法,才能读取数据。
IntBuffer intBuffer = IntBuffer.allocate(10);
intBuffer.put(1);
intBuffer.put(2);
//显示调用
intBuffer.flip();
while (intBuffer.hasRemaining()){
intBuffer.get();
}
这是很不友好的一点,因为很可能忘记调用flip而导致读取数据错误,netty基于这个缺点设计了两个指针来解决,readerIndex读索引,writerIndex写索引。
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
写数据的时候移动writerIndex,读取数据的时候移动readerIndex,当readerIndex于writerIndex相等时代表当前没有数据可读。
ByteBuf buffer = Unpooled.buffer();
for (int i = 0; i < 10; i++) {
buffer.writeInt(i);
}
//不需要显示调用,就可以进行读写操作
while (buffer.isReadable()){
System.out.println(buffer.readInt());
}
3.多种类型Buffer
netty提供了三种类型的Buffer
- HeapBuffer:分配在堆内的内存空间,它能在没有使用池化的情况下提供快速的分配和释放。
- DirectBuffer:分配在堆外的内存空间,由于是在堆外的内存空间,在网络中会减少数据的拷贝和上下文切换效率会比较高。直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。
- CompositeByteBuf:它为多个 ByteBuf 提供一个聚合视图。
netty相比nio中的Buffer多提供了一种Buffer类型CompositeByteBuf,复合缓冲区。
引用计数器
下面介绍一个比较重要的概念引用计数器,看一下Buffer的继承接口ReferenceCounted和Comparable
public abstract class ByteBuf implements ReferenceCounted, Comparable
Comparable比较大小的接口这个不用分析,重点是ReferenceCounted这个接口,引用计数器。
Since Netty version 4, the life cycle of certain objects are managed by their reference counts, so that Netty can return them (or their shared resources) to an object pool (or an object allocator) as soon as it is not used anymore. Garbage collection and reference queues do not provide such efficient real-time guarantee of unreachability while reference-counting provides an alternative mechanism at the cost of slight inconvenience.
上面是官网对引用计数器的介绍,将其翻译一下
自从Netty版本4以来,某些对象的生命周期是由它们的引用计数管理的,所以当它们不再被使用时,Netty可以将它们(或它们的共享资源)返回到对象池(或对象分配器)。垃圾收集和引用队列不能提供如此高效的不可达性实时保证,而引用计数提供了一种替代机制,但会带来一些不便。
由上面的描述可以知道引用计数是为池化的对象服务的,用于管理对象的生命周期,而其中提到会带来一些不便是指,我们需要显示的去释放对象,对我们java程序员来说是比较奇怪的,因为对象的管理回收都是交给GC去做的。
ReferenceCounted 的reference count 变化流程
- 初始化:当创建一个ReferenceCounted对象时,reference count为1;
- retain:增加reference count;
- release:减少reference count;
- 回收:当reference count减为0时,对象将被显示释放。
对引用计数器的修改,是需要保证线程安全的,下面简单分析它是怎么保证线程安全的修改reference count,主要分析AbstractReferenceCountedByteBuf这个类
refCnt
初始化一个volatile修饰的refCnt,值为1,表示对象创建时引用计数器为1
private volatile int refCnt = 1;
refCntUpdater
private static final AtomicIntegerFieldUpdater refCntUpdater
AtomicIntegerFieldUpdater这个类代码可以线程安全的修改volatile修饰的int类型的成员变量。
retain()
public ByteBuf retain() {
return retain0(1);
}
默认增加引用计数器加1
retain0()
private ByteBuf retain0(int increment) {
for (;;) {
int refCnt = this.refCnt;
//获得将要更新的值
final int nextCnt = refCnt + increment;
// Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow.
//如果当前更新的值小于等于increment,说明refCnt为0,对象不能再被引用
if (nextCnt <= increment) {
throw new IllegalReferenceCountException(refCnt, increment);
}
//使用refCntUpdater CAS更新引用计数器的值,直到成功
if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
break;
}
}
return this;
}
retain0
整体还是比较简单分为三步:
- 获取将要更新的值
- 判断当前的引用值是否为0,为0就说明对象不能再被引用
- CAS更新引用计算器
release()
private boolean release0(int decrement) {
for (;;) {
int refCnt = this.refCnt;
if (refCnt < decrement) {
throw new IllegalReferenceCountException(refCnt, -decrement);
}
//更新引用计数器
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
//表示引用计数器为0,回收资源
if (refCnt == decrement) {
deallocate();
return true;
}
return false;
}
}
}
release()
的流程也比较简单,需要注意的是deallocate这个方法,如果是非池化的Buffer直接释放就好,比如UnpooledHeapByteBuf中直接array = null,而池化的Buffer就需要再次把内存放入池中,以便循环使用。
BufferPool
为什么需要池化
池化的技术其实比较常见,比如数据库连接池、线程池。当有资源使用比较频繁而且创建和销毁比较消耗性能的场景,就可以考虑使用池化的技术,去管理资源。
为什么netty需要引入对象池呢?《Netty实战》的作者在一次技术分享中说到,因为DirectBuffer是直接分配在堆外的,我们无法保证GC对直接内存的及时回收。最关键的是jdk中对的DirectBuffer的创建流程如下:
- 分配一个直接内存需要进入一个静态同步方法;
- 如果内存不足会进入catch块,调用system.gc进行垃圾回收,然后进行100毫秒的线程sleep,之后再进行创建操作
DirectBuffer的销毁方法也是静态同步的同步。
可以看见DirectBuffer的创建和销毁的成本都是非常高的,所有引入了BufferPool对
DirectBuffer进行管理。
怎么实现
要实现对象的池化首先要向预先申请一大块内存,netty管理内存的类是PoolArena,主要结构是一个完全平衡的二叉树像堆一样,使用这种数据结构对内存进行管理。
而Recycler是实现对象池化的核心
/**
* Light-weight object pool based on a thread-local stack.
*
* @param the type of the pooled object
*/
public abstract class Recycler
基于thread-local和stack实现,当对象使用的时候就从栈弹出,对象回收的就是一个入栈的过程。这里就不详细的分析Netty池化的实现了。
回收对象
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}
总结
netty的buffer对比nio buffer优化了很多,将nio buffer中使用比较麻烦的地方做了优化,比如支持动态扩容、读写双指针。然后再对于buffer的使用的池化的技术去管理,解决buffer创建和销毁比较耗时的问题。netty源码也是一步一步的再改进,关于引用计数器也是netty4才有的,遇到问题,然后解决,我们要学习的就是解决问题的思路。