由于NIO编程的复杂性,ByteBuffer也有其局限性,主要缺点如下:
ByteBuf依然是Byte数组的缓冲区,Netty中ByteBuf的实现有两种策略:
ByteBuffer只有一个位置指针用于处理读写操作,而ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作用readerIndex,写操作用writeIndex。
readerIndex和writerIndex的取值一开始都是0,随着数的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex的就被视为discard,调用discardReadBytes方法,可以释放着部分空间,相当于ByteBuffer的compact方法。readerIndex和writerIndex之间的数据是可读的,相等于ByteBuffer的position和limit之间的数据。writerIndex和capacity之间的空间是可写的,相当于ByteBuffer的limit和capacity之间的可用空间。
由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,极大地简化了缓冲区的读写操作
ps:0到readerIndex之间是已经读取过的缓冲区,可用调用discardReadBytes操作来重用这一部分,以节约内存,防止ByteBuf的动态扩张
通常情况下,当我们对ByteBuffer进行put操作,如果缓冲区剩余可写空间不够,就会发生BufferOverflowException。ByteBuf对write操作进行了封装,由ByteBuf的write操作辅助进行剩余空间的检验,如果可用缓冲区(也就是writerIndex之后的空间)不足,ByteBuf会自动进行动态扩展。
public int writeBytes(InputStream in, int length) throws IOException {
//判断剩余空间
this.ensureWritable(length);
int writtenBytes = this.setBytes(this.writerIndex, in, length);
if (writtenBytes > 0) {
this.writerIndex += writtenBytes;
}
return writtenBytes;
}
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format("minWritableBytes: %d (expected: >= 0)", minWritableBytes));
} else if (minWritableBytes <= this.writableBytes()) {
return this;
} else if (minWritableBytes > this.maxCapacity - this.writerIndex) {
throw new IndexOutOfBoundsException(String.format("writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", this.writerIndex, minWritableBytes, this.maxCapacity, this));
} else {
int newCapacity = this.calculateNewCapacity(this.writerIndex + minWritableBytes);
this.capacity(newCapacity);
return this;
}
}
Discardable bytes
相比于其他Java对象,缓冲区的分配和释放是个耗时的操作,我们应该尽量重用它们。由于缓冲区的动态扩张需要进行字节数组的复制,是个耗时操作,为了提高性能,我们需要尽可能提升缓冲区的重用率。
假如缓冲区包含了N个整包消息,每个消息的长度为L,消息的可写字节数为R。当读取M个整包消息后,如果不对ByteBuf做压缩或者discardReadBytes操作,则可写的缓冲区长度依然为R。如果调用discardReadBytes操作,则可写的字节数会变为R = (R + M * L),之前已经读取的M个整包的空间会被重用。
ByteBuf的discardReadBytes操作之前:
操作之后:
Clear
Clear操作不会清空缓冲区内容本身,主要用来操作位置指针,如readerIndex和writerIndex,将它们还原为初始值。
Clear操作之前:
操作之后:
Mark和Rest
当对缓冲区进行读操作时,可能需要对之前的操作进行回滚。读操作不会改变缓冲区的内容,回滚操作主要是为了重新设置索引信息
对于JDK中的ByteBuffer,调用mark操作会将当前位置指针备份到mark变量中,当调用rest操作后,重新将指针的当前位置恢复为mark中的值。
Netty的ByteBuf也有类似的rest和mark接口,因为ByteBuf有读索引和写索引,对应此操作就有四种方法
public ByteBuf markReaderIndex() {
this.markedReaderIndex = this.readerIndex;
return this;
}
public ByteBuf resetReaderIndex() {
this.readerIndex(this.markedReaderIndex);
return this;
}
public ByteBuf markWriterIndex() {
this.markedWriterIndex = this.writerIndex;
return this;
}
public ByteBuf resetWriterIndex() {
this.writerIndex = this.markedWriterIndex;
return this;
}
Derived buffers
类似数据库的视图,ByteBuf提供多个接口用于创建某个ByteBuf的视图或者复制ByteBuf
*如何将ByteBuf转换成ByteBuffer
经验表明:ByteBuf的最佳实践是在IO通信的线程的读写缓冲区使用直接内存,后端业务消息的编解码模块使用堆内存
2.从回收的角度看,ByteBuf也可以分为两类:
3.从Unsafe和非Unsafe分类
主要成员变量
//leakDetector被定义为static,意味着所有的ByteBuf实例共享一个ResourceLeakDetector对象,这个对象用于检测对象是否内存泄漏
static final ResourceLeakDetector<ByteBuf> leakDetector = new ResourceLeakDetector(ByteBuf.class);
读操作簇
public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {
this.checkReadableBytes(length);//读之前,首先对缓冲区的可用空间进行校验
this.getBytes(this.readerIndex, dst, dstIndex, length);//校验通过,调用getBytes方法,从当前读索引开始,复制length个字节到目标byte数组中
this.readerIndex += length;//维护读索引
return this;
}
protected final void checkReadableBytes(int minimumReadableBytes) {
this.ensureAccessible();
if (minimumReadableBytes < 0) {//如果读取的长度小于0,则抛出异常
throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
} else if (this.readerIndex > this.writerIndex - minimumReadableBytes) {//如果可读的字节数小于需要读取的长度,则抛出异常
throw new IndexOutOfBoundsException(String.format("readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s", this.readerIndex, minimumReadableBytes, this.writerIndex, this));
}
}
写操作簇
public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) {
this.ensureWritable(length);//计算动态扩张的容量
this.setBytes(this.writerIndex, src, srcIndex, length);//重新创建缓冲区
this.writerIndex += length;//维护写索引
return this;
}
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format("minWritableBytes: %d (expected: >= 0)", minWritableBytes));
} else if (minWritableBytes <= this.writableBytes()) {
return this;
} else if (minWritableBytes > this.maxCapacity - this.writerIndex) {//如果写入的字节数组长度大于可以动态扩展的最大可写字节数,则抛出异常
throw new IndexOutOfBoundsException(String.format("writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", this.writerIndex, minWritableBytes, this.maxCapacity, this));
} else {//如果当前写入的字节数组虽然大于目前的ByteBuf的可写字节数,但是可以通过动态扩展满足,则进行动态扩展
int newCapacity = this.calculateNewCapacity(this.writerIndex + minWritableBytes);//计算新的容量
this.capacity(newCapacity);
return this;
}
}
/**
* 首先设置门限阈值为4M(用于倍增和步进的分界值),当需要的新容量正好等于门限阈值,则使用阈值作为新的缓冲区容量。
* 如果新申请的内存空间大于阈值,不能采用倍增的方式(防止内存膨胀和浪费)扩张内存,采用每次
* 步进4M的方式进行内存扩张。扩张的时候需要对扩张后的内存和最大内存(maxCapacity)比较,如果
* 大于缓冲区的最大长度,则使用maxCapacity作为扩容后的缓冲区容量
* 如果扩容后的新容量小于阈值,则以64为计数进行倍增,直到倍增后的结果大于或等于需要的容量值
*
* 采用倍增或者步进算法的原因如下:如果仅仅以minNewCapaciy作为目标容量,则本次扩容后的可写
* 字节刚好够本次写入使用。写入完成后,它的可写字节数会变为0,下次做写入操作时,需要再次动态
* 扩张,而频繁的内存复制会导致性能下降
* 采用先倍增后步进的原因如下:当内存比较小的时候,倍增操作并不会带来太多内存浪费,例如64字
* 节-128节字-156字节。但是,当内存增长到一定阈值后,再进行倍增就可能会带来额外的内存浪费,
* 如10M-20M-40M。因此达到某个阈值后就需要以步进的方式对内存进行平滑地扩张
*/
private int calculateNewCapacity(int minNewCapacity) {
int maxCapacity = this.maxCapacity;
int threshold = 4194304;
if (minNewCapacity == 4194304) {
return 4194304;
} else {
int newCapacity;
if (minNewCapacity > 4194304) {
newCapacity = minNewCapacity / 4194304 * 4194304;
if (newCapacity > maxCapacity - 4194304) {//如果newCapacity大于maxCapacity - 4M。说明超出增大的长度了,最大只能maxCapacity
newCapacity = maxCapacity;
} else {
newCapacity += 4194304;
}
return newCapacity;
} else {
for(newCapacity = 64; newCapacity < minNewCapacity; newCapacity <<= 1) {
}
return Math.min(newCapacity, maxCapacity);
}
}
}
重用缓冲区
public ByteBuf discardReadBytes() {
this.ensureAccessible();
if (this.readerIndex == 0) {//首先对读索引进行判断,如果为0则说明没有可重用的缓冲区,直接返回
return this;
} else {
/*
* 说明缓冲区中既有已经读取过的被废弃的缓冲区,也有尚未读取的缓冲区
* 调用setBytes()方法进行字节数组复制,将尚未读取的字节数组复制到缓冲区的起始位置,然后
* 重新设置读写索引,读索引设置为0,写索引设置为值之前的写索引长度减去读索引(重用的缓
* 冲区长度)
*/
if (this.readerIndex != this.writerIndex) {
this.setBytes(0, this, this.readerIndex, this.writerIndex - this.readerIndex);
this.writerIndex -= this.readerIndex;
/**
*设置读写索引的同时,需要同时调整markedReaderIndex和markedWriterIndex
*
*/
this.adjustMarkers(this.readerIndex);
this.readerIndex = 0;
} else {
this.adjustMarkers(this.readerIndex);
this.writerIndex = this.readerIndex = 0;
}
return this;
}
}
/**
*首先对备份的markedReaderIndex、markedWriterIndex、和需要减少的decrement进行判断,如果小
*于需要减少的值,则置为0;否则新值为旧值减去decrement之后的值
*
*/
protected final void adjustMarkers(int decrement) {
int markedReaderIndex = this.markedReaderIndex;
if (markedReaderIndex <= decrement) {
this.markedReaderIndex = 0;
int markedWriterIndex = this.markedWriterIndex;
if (markedWriterIndex <= decrement) {
this.markedWriterIndex = 0;
} else {
this.markedWriterIndex = markedWriterIndex - decrement;
}
} else {
this.markedReaderIndex = markedReaderIndex - decrement;
this.markedWriterIndex -= decrement;
}
}
从类的名字就可以看出该类主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,做自动内存回收。
成员变量
//refCntUpater是AtomicIntegerFieldUpdater类型变量,通过原子的方式对成员变量进行更新等操作,实现线程安全
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
//用于标识refCnt字段在AbstractReferenceCountedByteBuf中的内存地址
private static final long REFCNT_FIELD_OFFSET;
//跟踪对象的引用次数
private volatile int refCnt = 1;
对象引用计数器
//CAS操作
public ByteBuf retain() {
int refCnt;
do {
refCnt = this.refCnt;
if (refCnt == 0) {
throw new IllegalReferenceCountException(0, 1);
}
if (refCnt == 2147483647) {
throw new IllegalReferenceCountException(2147483647, 1);
}
} while(!refCntUpdater.compareAndSet(this, refCnt, refCnt + 1));
return this;
}
释放引用计数器
public final boolean release() {
int refCnt;
do {
refCnt = this.refCnt;
if (refCnt == 0) {
throw new IllegalReferenceCountException(0, -1);
}
} while(!refCntUpdater.compareAndSet(this, refCnt, refCnt - 1));
if (refCnt == 1) {//等于1意味着申请和释放相等,说明对象引用已经不可达,需要被释放
this.deallocate();
return true;
} else {
return false;
}
}
PoolArea
Arena本身是指一块区域,在内存管理中,Memory Arena是指内存中的一大块连续的区域,PoolArena就是Netty的内存池实现类。
Netty的PoolArea是由多个Chunk组成的大块内存区域,而每个Chunk则由一个或者多个Page组成,因此对内存的组织和管理主要集中在如何管理和组织Chunk和Page了
PoolChunk
Chunk主要用来组织和管理多个Page的内存分配和释放,在Netty中,Chunk中的Page被构建成一棵二叉树。
假设一个Chunk由16个Page组成,这些Page会按照下图的形式组织起来:
Page的大小是4个字节,Chunk的大小是64个字节(4 * 16)。整棵树有5层,第1层(也就是叶子节点所在的层)用来分配所有Page的内存,第4层用来分配2个Pager的内存。
每个节点都记录了自己在整个Memory Arena中的偏移地址,当一个节点代表的内存区域被分配出去之后,这个节点就会被标记为已分配,自这个节点以下的所有节点在后面的内存分配请求都会被忽略。
PoolSubPage
对于小于一个Page的内存,Netty在Page中完成分配。每个Page都会被切分成大小相等的多个存储块,存储块的大小是由第一次申请的内存块大小决定的。例如一个Page是8个字节,如果第一次申请的块是4个字节,那么这个Page就包含2个存储块;如果第一次申请的是8个字节,那么这个Page就被分配成1个存储块。
Page中存储区域的使用状态通过一个long数组维护,数组中的每一位表示每块存储区域的占用情况:0表示未占用,1表示已占用。
无论是Chunk还是Page,都通过状态位来标识内存是否可用,不同之处在于Chunk通过二叉树对节点进行标识实现,Page是通过维护块的使用状态来标识实现。
ByteBufAllocator是字节缓冲区分配器,按照Netty的缓冲区的实现不同,共有两种不同的分配器:
public interface ByteBufAllocator {
ByteBuf buffer(); //分配一个字节缓冲区,缓冲区的类型由ByteBufAllocator的实现类决定
ByteBuf buffer(int var1);//初始容量
ByteBuf buffer(int var1, int var2);//初始容量,最大容量
ByteBuf ioBuffer();//分配一个内存缓冲区,更希望是直接内存,因为直接内存的IO操作性更高
ByteBuf ioBuffer(int var1);
ByteBuf ioBuffer(int var1, int var2);
ByteBuf heapBuffer();//分配堆内存缓冲区
ByteBuf heapBuffer(int var1);
ByteBuf heapBuffer(int var1, int var2);
ByteBuf directBuffer();//分配直接内存缓冲区
ByteBuf directBuffer(int var1);
ByteBuf directBuffer(int var1, int var2);
CompositeByteBuf compositeBuffer();
CompositeByteBuf compositeBuffer(int var1);
CompositeByteBuf compositeHeapBuffer();
CompositeByteBuf compositeHeapBuffer(int var1);
CompositeByteBuf compositeDirectBuffer();
CompositeByteBuf compositeDirectBuffer(int var1);
boolean isDirectBufferPooled();//是否使用了直接内存内存池
}
AbstractByteBufAllocator已经实现了大部分的代码,具体关于heap和direct以及unsafe和非unsafe的实现,需要由继承AbstractByteBufAllocator的子类实现。
/**
* 关于unsafe和非unsafe的内存分配自行判断是否为unsafe对象
*
*/
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
protected UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
this(alloc, new byte[initialCapacity], 0, 0, maxCapacity);//heap内存是直接new byte[]的
}
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
Object buf;
if (PlatformDependent.hasUnsafe()) {//判断是否为unsafe对象
buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer((ByteBuf)buf);
}
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = (PoolThreadCache)this.threadCache.get();//拿到线程局部缓存
PoolArena<byte[]> heapArena = cache.heapArena;//在线程局部缓存的Area进行堆内存分配
Object buf;
if (heapArena != null) {
buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer((ByteBuf)buf);
}
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = (PoolThreadCache)this.threadCache.get();//拿到线程局部缓存
PoolArena<ByteBuffer> directArena = cache.directArena;//在线程局部缓存的Area进行直接内存分配
Object buf;
if (directArena != null) {
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else if (PlatformDependent.hasUnsafe()) {
buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer((ByteBuf)buf);
}