用NIO时需要使用缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer,而主要使用的是ByteBuffer。但是ByteBuffer也有局限性:
1.ByteBuffer长度固定,一旦分配完成,它的容量就不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发送索引越界异常;
2.ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
3.ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程。
为弥补这些不足,Netty提供了自己的ByteBuffer实现——ByteBuf。ByteBuf依然是个Byte数组的缓冲区,它的基本功能应该与JDK的ByteBuffer一致,提供以下几类基本功能:
1. 7种Java基础类型、byte数组、ByteBuffer等的读写;
2. 缓冲区自身的copy和slice等;
3. 设置网络字节序;
4. 构造缓冲区实例;
5. 操作位置指针等方法;
readerIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex的就被视为discard的,调用discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。ReaderIndex和writeIndex之间的数据是可读取的,等价于ByteBuffer的positon和limit之间的数据。writerIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间。
由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常.
对于JDK的ByteBuffer,调用mark操作会将当前的位置指针备份到mark变量中,当调用reset操作之后,重新将指针的当前位置恢复为备份在mark中的值。
Netty的ByteBuf也有类似的接口。因为ByteBuf有读索引和写索引,因此,它总共有4个相关的方法:markReaderIndex、resetReaderIndex、markWriterIndex、resetWriterIndex。
通常情况下,当对ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOberflowException异常。为避免发生这个问题,通常在进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要重新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer。
ByteBuf对write操作进行封装,由ByteBuf的write操作负责进行剩余可用空间的校验,如果可用缓冲区不足,ByteBuf会自动进行动态扩展。
1. 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收;缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内存Channel中,性能会有一定程度的下降;
2. 直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配,相对于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。
最佳实践:在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf。
1.ByteBuffer长度固定,一旦分配完成,它的容量就不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发送索引越界异常;
2.ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
3.ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程。
为弥补这些不足,Netty提供了自己的ByteBuffer实现——ByteBuf。ByteBuf依然是个Byte数组的缓冲区,它的基本功能应该与JDK的ByteBuffer一致,提供以下几类基本功能:
1. 7种Java基础类型、byte数组、ByteBuffer等的读写;
2. 缓冲区自身的copy和slice等;
3. 设置网络字节序;
4. 构造缓冲区实例;
5. 操作位置指针等方法;
readerIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex的就被视为discard的,调用discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。ReaderIndex和writeIndex之间的数据是可读取的,等价于ByteBuffer的positon和limit之间的数据。writerIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间。
由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常.
需要注意的是,调用discardReadBytes会发生字节数组的内存复制,所以频繁调用将会导致性能下降。
对于JDK的ByteBuffer,调用mark操作会将当前的位置指针备份到mark变量中,当调用reset操作之后,重新将指针的当前位置恢复为备份在mark中的值。
Netty的ByteBuf也有类似的接口。因为ByteBuf有读索引和写索引,因此,它总共有4个相关的方法:markReaderIndex、resetReaderIndex、markWriterIndex、resetWriterIndex。
通常情况下,当对ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOberflowException异常。为避免发生这个问题,通常在进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要重新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer。
ByteBuf对write操作进行封装,由ByteBuf的write操作负责进行剩余可用空间的校验,如果可用缓冲区不足,ByteBuf会自动进行动态扩展。
ByteBuf的主要类继承关系:
1. 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收;缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内存Channel中,性能会有一定程度的下降;
2. 直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配,相对于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。
最佳实践:在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf。
从内存回收角度看,ByteBuf分2类:
基于对象池的ByteBuf和普通ByteBuf。
两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护一个内存池,可以循环利用创建的ByteBuf,提高内存使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。