Netty篇:ByteBuf部分源码分析
缓冲区(Buffer)
缓冲区(Buffer)就是在内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区:
使用缓冲区可以减少实际的物理读写次数,而且缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数
ByteBuffer
常用缓冲区就是JDK NIO类库提供的java.nio.Buffer,7种数据类型(Boolean除外)均有自己的缓冲区实现,NIO编程主要用ByteBuffer,ByteBuffer完全可以满足NIO编程的需要,但是由于NIO编程的复杂性,ByteBuffer也有局限性,主要缺点有:
(1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常
(2)ByteBuffer只有一个标识位置的指针position,读取的时候需要手工调用flip()和rewind()等
(3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持
BufferBuf
BufferBuf是个抽象类,其子类非常多。
从内存分派角度讲,ByteBuf可以分为两类:
(1)HeapByteBuf:堆内存字节缓冲区,特点是内存分配和回收速度快,可以被JVM自动回收;缺点就是如果进行了Socket的I/O读写,需要额外做一次内尺寸复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程序下降
(2)DirectByteBuf:直接缓冲区的字节数组位于JVM堆外的NATIVE堆,由操作系统管理申请和释放,而DirectByteBuf的引用由JVM管理。直接缓冲区由操作系统管理,一方面,申请和释放效率都低于堆缓冲区,另一方面,却可以大大提高IO效率。由于进行IO操作时,常规下用户空间的数据(JAVA即堆缓冲区)需要拷贝到内核空间(直接缓冲区),然后内核空间写到网络SOCKET或者文件中。如果在用户空间取得直接缓冲区,可直接向内核空间写数据,减少了一次拷贝,可大大提高IO效率,这也是常说的零拷贝。
从内存回收角度讲,ByteBuf可以分为两类:
(1)UnpooledByteBUf:不使用对象池的缓冲区,不需要创建大量缓冲区对象时建议使用该类缓冲区
(2)PooledByteBuf:对象缓冲区,当对象释放后会归还给对象池,所以可循环利用,提升内存的使用效率,降低由于高负载导致的频繁GC,当需要大量且频繁创建缓冲区时,建议使用该类缓冲区
在这里插入图片描述
AbstractByteBuf
AbstractByteBuf继承自ByteBuf,ByteBuf的一些公共属性和功能会在AbstractByteBuf中实现,比如读写索引以及标记索引的维护、容量扩增以及废弃字节丢弃等公共功能,例如:
定义读写索引以及标记索引
在这里插入图片描述读操作
在这里插入图片描述 在这里插入图片描述首先对缓冲区的可用空间进行校验,从当前读索引开始,复制length个字节到目标byte数组中。由于不同的子类复制操作的技术实现细节不同,因为该方法由子类实现。如果读取成功,需要对读索引进行递增:readerIndex+=length
写操作
在这里插入图片描述 在这里插入图片描述首先对写入字节数字的长度进行合法性校验,如果当前写入的字节数组长度虽然大于目前ByteBuf的可写字节数,当时通过自身的动态扩展可以满足新的写入请求,则进行动态扩展,计算扩容代码如下:
在这里插入图片描述如果扩容后的新容量小于阈值,则以64为计数进行倍增,知道被增厚的结果大于或等于到需要的容量值。
采用倍增或者步进算法的原因:如果以minNewCapacity作为目标容量,则本次扩容后的可写字节数刚好够本子写入使用。写入完成后,可写字节数变为0,下次做写入操作是,仍需扩容,由于扩容需要进行内存复制,频繁的内存复制会导致性能下降。
采用先倍增后步进的原因:当内存比较小的情况下,倍增操作并不会带来太多的内存浪费,当内存增长到一定的阈值后,在进行倍增就可能带来额外的内存浪费。
重新计算完动态扩张后的目标容量后,需要重新创建新的缓冲区,将原缓冲区的内容复制到新创建的ByteBuf中,最后设置读写索引和mark标签等,由于子类对应不同的复制操作,所以抽象方法,由子类实现。
重用缓冲区
在这里插入图片描述 在这里插入图片描述首先对读索引进行判断,如果为0则表示没有可重用的缓冲区,直接返回。如果读索引大于0且读索引不等于写索引,说明缓冲区中有已经读取过被丢弃的缓冲区,也有尚未读取的缓冲区,。调用setBytes方法进行字节数组复制。将尚未读取的字节数复制到缓冲区的起始位置,然后重新设置读写索引,读索引设置为0,写索引设置为之前的写索引减去读索引。
在这里插入图片描述在设置读写索引的同时,需要调整markedReaderIndex和markedWriterIndex。
如果readerIndex等于writeIndex则说明没有可读的字节数组,就不需要进行内存复制,直接调整mark,将读写索引设置为0进而可完成缓冲区的重用。
AbstractReferenceCountedByteBuf
抽象类继承自AbstractByteBuf,主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,作自动内存回收。
在这里插入图片描述ReferenceCountUpdater类的updater()和unsafeOffset()方法是抽象方法,需要具体的子类提供实现。AtomicIntegerFieldUpdater是JDK提供的一个可以通过原子更新的方式修改指定字段的工具。
ReferenceCountUpdater类的功能是可以通过CAS的方式直接修改某个类的一个字段的值。
ReferenceCountUpdater#retain 增加引用计数的值:
在这里插入图片描述
委派update()类对refCnt用CAS操作加2(refCnt是偶数则表示当前缓冲区的状态为正常状态,如果refCnt是奇数则表示缓冲区的状态为待销毁状态。缓冲区引用计数的真实值为refCnt/2),然后做缓冲区释放和溢出校验。
ReferenceCountUpdater#release 减少引用计数的值:
在这里插入图片描述
获取instance实例中保存的rawCnt值,并计算出真实的refCnt值,即realCnt,如果要减少的引用值和真实的refCnt值相同,也即需要释放缓冲区对象,则调用tryFinalRelease0方法将refCnt的数值修改为1(只要修改为奇数即可),如果要减少的引用值小于真实的refCnt值,则通过cas修改refCnt的值,调用Thread.yield()方法释放出CPU的执行权,因为修改引用计数的逻辑在整个系统逻辑的优先级并不高,所以让出执行权有利于提高高并发下的系统吞吐量。
参考:
《Netty权威指南》