ByteBuf简介
ByteBuf提供两个指针变量readerIndex和writerIndex分别地来支持顺序进行读写操作。
discardable bytes表示已读过可丢弃的数据,readable bytes表示实际的数据存储内容,writable bytes表示可写入的部分。
ByteBuf类型
Netty ByteBuf提供三种缓冲区类型:
1、heap buffer
这是最常用的类型,heap buffer这种缓冲区是分配在堆上面的,直接由Java虚拟机负责垃圾回收,并且将实际的数据存放在byte array中来实现。
优点:由于这种缓冲区是分配在堆上,因此可以快速的创建与释放。
缺点:每次读写数据时,都需要将数据复制到直接缓冲区再进行网络传输。
2、direct buffer
在堆之外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,由操作系统在本地内存进行数据分配。但direct buffer的JAVA对象是归GC管理的,只要GC回收了它的JAVA对象,操作系统才会释放direct buffer所申请的空间
优点:在使用socket进行数据传输时,因为数据直接位于操作系统的本地内存中,所以不需要将数据从jvm复制到直接缓冲区中,性能好。
缺点:因为direct buffer是直接在操作系统内存中的,所以内存空间的分配与释放要比堆空间更加复杂,而且速度要慢一些。
3、composite buffer(复合缓冲区)
小结:对于后端业务消息的编解码来说推荐使用HeapByteBuf,对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf。
JDK的ByteBuffer的缺点
1、final byte[] hb;这是JDK的ByteBuf中用于存储数据的声明;可以看到,其字节数组是被声明为final的,也就是长度是固定不变的。一旦分配好后不能动态扩容与收缩;如果ByteBuffer的空间不足,我们只有一种解决方案,创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer中的数据复制过去,这一切操作都需要由开发者自己手动完成。
2、ByteBuffer只使用一个position指针来标识位置信息,在进行读写切换时就需要调用flip方法,使用起来很不方便。
Netty的ByteBuf的优点
1、存储字节的数组是动态,其最大值默认是Integer.MAX_VALUE。这里的动态性体现在write方法中,write方法在执行时会判断buffer容量,如果不足则自动扩容。
2、ByteBuf的读写索引完全分离,使用起来方便。
引用计数
从Netty版本4开始,某些对象的生命周期由它们的引用计数来管理,以便Netty可以在不再使用它们时将它们返回到对象池。垃圾收集无法提供如此高效的实时可达性保证,而引用计数则提供了一种替代机制。
ByteBuf使用了引用计数来改进分配内存和释放内存的性能。ByteBuf 实现了ReferenceCounted 接口,在其重要实现类AbstractReferenceCountedByteBuf中可以发现:
1、计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
2、所有ByteBuf的引用计数器初始值为1,调用release(),将计数器减1;调用retain(),将计数器加1;当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。
retain0(int increment)中使用了自旋锁,一旦原子更新器将新值更新成功(CAS),跳出,否则进入自旋。
ByteBuf的释放
通常的经验法则是谁最后访问(access)了引用计数对象,谁就负责销毁(destruction)它。具体来说是以下两点:
a、如果组件(component)A把一个引用计数对象传给另一个组件B,那么组件A通常不需要销毁对象,而是把决定权交给组件B。
b、如果一个组件不再访问一个引用计数对象了,那么这个组件负责销毁它。
自动释放
通过 tailHandler 自动释放入站 ByteBuf
继承 SimpleChannelInboundHandler 的完成 入站ByteBuf 自动释放
通过headHandler自动释放出站 ByteBuf
这是SimpleChannelInboundHandler自动释放的源码实现,channelRead0(ctx, imsg)封装了用户的业务代码,在finally代码块中调用释放,整体上使用了模板方法设计模式。
入站处理流程中,如果对原消息不做处理,默认会调用 ctx.fireChannelRead(msg) 把原消息往下传,由pipeline最后的 TailHandler 完成自动释放。
如果截断了入站处理流水线,则可以继承 SimpleChannelInboundHandler ,完成入站ByteBuf 自动释放。
出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。
出站处理用到的 Bytebuf 缓冲区,一般是要发送的消息,通常由应用所申请。在出站流程开始的时候,通过调用 ctx.writeAndFlush(msg),Bytebuf 缓冲区开始进入出站处理的 pipeline 。在每一个出站Handler中的处理完成后,最后消息会来到出站的最后一站 HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。
手动释放
入站处理中,如果将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
入站处理中,如果已经不再调用 ctx.fireChannelRead(msg) 传递任何消息,也没有继承SimpleChannelInboundHandler 完成自动释放,那更要把原消息release掉;