type
status
date
slug
summary
tags
category
password
1、ByteBuf是什么
Java IO 的基本单位是字节,我们需要一个容器来存储这些字节数据。通常有下面几种方式。
1.1 Stream
在传统 Java BIO 中,我们可以使用
InputStream 和 OutputStream 作为字节的容器。1.2 Bytebuffer
Java NIO(New IO) 是从 Java 1.4 版本开始引入的一个新的 IO API,支持面向缓冲区的、基于通道的IO操作,这些类都放在 java.nio 包下。Java NIO 提供了
Bytebuffer 作为操作字节数据的容器。下面是一个简单例子。从上面例子看出,操作
Bytebuffer 并不方便。因为 Bytebuffer 只有一个索引 position,是读写共用的。从写模式改成读模式需要调用 flip() 把 position 重置为 0,从读模式改成写模式也需要调用 clear() 把 position 重置为0。除此之外,Bytebuffer 不能扩容,每次 put 操作时,都会对可用空间进行校检,如果剩余空间不足,需要重新创建一个新的 ByteBuffer,然后将旧的 ByteBuffer 复制到新的 ByteBuffer 中去。
1.3 ByteBuf
ByteBuf 是 Netty 的数据容器,相比起 NIO 原生的数据容器 ByteBuffer,ByteBuf 做了许多优化和改进:
- Bytebuf 维护了 readIndex 和 writeIndex 两个 index,在读写模式转换不需要调用 flip()。
- 支持自动扩容。
- 支持方法链接调用。
- 支持引用计数。
- 支持池技术(比如:线程池、数据库连接池)。
- 通过用其内置的复合缓冲区可实现透明的零拷贝。
2、ByteBuf原理
2.1 ByteBuf的结构
ByteBuf 本质是一个由不同的索引分别控制读访问和写访问的字节数组。以下是官方提供的 ByteBuf 的结构图:
ByteBuf 包含三个参数:
- readerIndex:读索引,不超过 writerIndex,调用
readXXX()或skipBytes()方法会移动该索引,调用getXXX()方法则不会移动。
- writerIndex:写索引,不超过 capacity,调用
writeXXX()方法会移动该索引,调用setXXX()则不会移动。
- capacity:容量,超过 capacity 后会自动扩容,最大容量不超过 Integer.MAX_VALUE。
ByteBuf 被起始索引和这三个参数所在的索引分成三部分:
- discardable bytes(可丢弃字节):范围为 0 ~ readerIndex,这部分字节数据已经被读取完,空间等待回收。
- readable bytes(可读字节):范围为 readerIndex ~ writerIndex,这部分字节数据已经准备好,等待用户读取。
- writable bytes(可写字节):范围为 writerIndex ~ capacity,这部分空闲空间可写入字节。
2.2 ByteBuf的的工作过程
1、初始化
ByteBuf 初始化后 readerIndex 和 writeIndex 的取值一开始都是 0。
2、写入N(N < capacity)个字节后,writerIndex = N。
3、读取M(M<N)个字节之后,readerIndex = M。
4、调用
discardReadBytes() 丢弃 0 ~ readerIndex 间的数据,相当于 readerIndex 和 writerIndex 同时向前移动 M 个字节,最后 readerIndex = 0,writerIndex = N - M。5、当我们已经读取完所有的数据后,可以调用
clear() 使 ByteBuf 恢复到初始状态,这时候 readerIndex = writerIndex = 0。注意
- 调用
clear()的开销没有discardReadBytes()那么大,因为它不需要任何内存复制。
- readerIndex 和 writerIndex 超过边界值,会发生 IndexOutOfBoundException 异常。
3、ByteBuf的内存管理
ByteBuf 按照数据的存储位置,可以分为
堆缓冲区(HEAP BUFFER) 和 直接缓冲区(DIRECT BUFFER) 。- 堆缓冲区(HEAP BUFFER):将数据存储在 JVM 的堆内存中,这些内存需要被 jvm 管理。
- 直接缓冲区(DIRECT BUFFER):将数据存在 JVM 的堆内存外面,这些内存不需要被 jvm 管理。
按内存对象是否可以重复利用,可以分为
池化内存(Pooled) 和 非池化内存(Unpooled)。- 池化内存(Pooled):每次都从预先分配好的内存中去取出一段连续内存封装成一个 ByteBuf 给应用程序使用。
- 非池化内存(Unpooled):每次分配内存的时候,直接调用系统 api,向操作系统申请一块内存。
3.1 堆内存和直接内存
3.1.1 堆缓冲区
最常用的模式,这种模式直接将数据存储在 JVM 的堆空间中,这种情况下 ByteBuf 数组被称为支撑数组(backing array)。
优点:能在没有使用池化的情况下提供快速的分配和释放,非常适合于有遗留的数据需要处理的情况。缺点:在 socket 发送前,需要先把数据拷贝到直接缓冲区,导致 IO 效率不高。
3.1.2 直接缓冲区
将数据存储在 JVM 堆以外的内存,需要调用系统级别的 API
优点:免去中间交换的内存拷贝,提高 IO 速度。缺点:分配和释放代价大,而且这部分内存不受 JVM 管理。需要主动释放。
Netty 默认使用直接缓冲区(直接缓冲区创建和销毁的性能问题,可通过下面的池化技术解决)。如果想切换成堆缓冲区,需要设置下面的参数。
3.2 池化内存和非池化内存
3.3.1 池化内存
Netty 会先向系统申请一大块内存,通过池化算法管理这块内存,并向上层提供申请内存接口。我们需要使用内存的时候,再通过接口申请一段连续内存封装成一个 ByteBuf 给应用程序使用。池化的最大意义在于可以重用 ByteBuf。
优点:池对象可以回收复用,提高内存分配的效率。缺点:需要对预先申请的内存进行管理。
3.2.2 非池化内存
同 java 对象的创建销毁过程,直接向 JVM 申请内存创建 ByteBuf 对象,使用完后交给 JVM 进行 gc 回收。
优点:使用简单缺点:对象不能复用,创建销毁代价很大。
池化功能是否开启,可以通过下面的系统环境变量来设置
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化功能还不成熟,默认是非池化实现
4、ByteBuf 使用
4.1 ByteBuf常用API
4.1.1 ByteBuf读写
主要分两类:
readXXX()和writeXXX()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整 。
getXXX()和setXXX()操作,从给定的索引开始,并且保持索引不变。
其他操作
isReadable ()如果至少有一个字节可供读取,则返回 true
isWritable ()如果至少有一个字节可被写入,则返回 true
readableBytes()返回可被读取的字节数
writableBytes()返回可被写入的字节数
capacity()返回 ByteBuf 可容纳的字节数 。在此之后,它会尝试再次扩展直到达到maxCapacity ()
maxCapacity()返问 ByteBuf 可以容纳的最大字节数
hasArray()如果 ByteBuf 由一个字节数组支撑,则返回 true
array ()如果 ByteBuf 由一个字节数组支撑则返问该数组;否则,它将抛出一个 UnsupportedOperat工onException 异常
4.1.2 ByteBuf引用计数
对于非池化类型的 bytebuffer,例如
UnpooledHeapByteBuf 和 UnpooledDirectByteBuf ,能够依赖 JVM GC 回收器自动回收。而对于池化类型的 bytebuffer,例如
PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的 bytebuf 放回池里,如果不释放,内存池会越来越大,直到内存溢出。所以所有的 ByteBuf(包括池化和非池化)需要在 JVM 的 GC 机制之外,有自己的引用计数器和回收过程(主要是回收到netty申请的内存池)。这个计数器机制如下:
- 所有 ByteBuf 的引用计数器初始值为1。
- 可以增加/减少 ByteBuf 的引用计数器,
- 当 ByteBuf 的引用计数器为 0 时该对象就会被释放内存或回收到内存池。
- 访问一个被释放的 ByteBuf 会抛 IllegalReferenceCountException 异常。
ByteBuf 引用计数器的常用方法有:
release():计数器减1。从 InBound 里读取的 ByteBuf 和自己创建的 ByteBuf 要自己调用release()手动释放,写入到 OutBound 的 Bytebuf 例如调用writeAndFlush()由 netty 负责释放,不需要手动调用release()。
retain():计数器加1。在使用派生缓冲区的时候,为了防止源 ByteBuf 突然被释放导致派生缓冲区操作异常,需要调用retain()来显示派生缓冲区的存在。
refCnt():返回计数器的值。
4.1.3 派生缓冲区
ByteBuf 还提供了一种视图的方式来读写内容,这种视图被称作派生缓冲区。
派生缓冲区维护单独的 readIndex、writeIndex 和 markIndex,但与原生 ByteBuf 共享数据和引用计数器。这意味着派生缓冲区的创建开销很低,但是如果修改了其他内容,也同时修改对应的原生 ByteBuf。
派生缓冲区的常用方法有:
copy():返回源 ByteBuf 的完全拷贝。该拷贝和源 ByteBuf 完全独立,在新 ByteBuf 上修改不会影响旧 ByteBuf。
duplicate():返回源 ByteBuf 的整个空间的共享缓冲区,该缓冲区的 readIndex、writeIndex、capacity 的位置和源 ByteBuf 一致,但是一套独立的索引,读写操作不会影响源 ByteBuf 的索引。该方法内部不会调用retain()方法,所以计数器不会增加。
slice():只返回源 ByteBuf 已经写入数据区域的共享缓冲区。和duplicate()方法不同,slice()方法只返回源 ByteBuf 已写入数据部分的拷贝,所以 readIndex、writeIndex、capacity 的位置都是重置的,也是一套独立的索引。该方法内部不会调用retain()方法,所以计数器不会增加。
输出如下:
4.2 ByteBuf工具类
4.2.1 ByteBufAllocator
Netty 为创建 ByteBuf 实例专门提供了一个接口
ByteBufAllocator,适用于可以获取到 channelHandlerContext 或 channel 实例的场景。实现类有两个PooledByteBufAllocator返回池化实例。
UnpooledByteBufAllocator返回非池化实例。
Netty 默认使用了池化的
ByteBufAllocator 。但我们可以在 Bootstrap 配置参数来自定义使用池化还是非池化分配器。使用 PooledByteBufAllocator 创建 ByteBuf
使用 UnpooledByteBufAllocator 创建 ByteBuf
4.2.2 Unpooled
提供了大量静态方法来创建缓冲区的工具类。该工具实际上是通过调用
UnpooledByteBufAllocator 来实现最终缓冲区的创建工作的。静态方法主要分为以下三类:- 创建新的缓冲区
- 基于已有缓冲区、byte数组或字符串创建一个wrapper缓冲区
- 基于已有缓冲区创建一个副本缓冲区
Unpooled 使得 ByteBuf 同样可用于那些并不需要Netty的其他组件的非网络项目。当没有办法获取到 channelHandlerContext 或 channel 实例的时候,可以使用 Unpooled 类来创建 ByteBuf。4.2.3 ByteBufUtil
Unpooled 工具类的关注点是缓冲区的创建,而 ByteBufUtil 工具的类的关注点是对于已有的缓冲区的操作,如打印(hexDump)、编码、解码、拷贝等。4.2.4 CompositeByteBuf
CompositeByteBuf 提供了将多个 buffer 虚拟成一个合并的 Buffer 的技术。CompositeByteBuf 中的 ByteBuf 实例可能同时包含堆缓冲区的和直接缓冲区的。如果 CompositeByteBuf 只含有一个实例,调用 hasArray() 方法会返回这个实例的 hasArray() 方法的值;否则总是返回 false。
例如一条消息包含 header 和 body 两部分,每条消息需要使用不同的 header 搭配同一个 body,就可以使用复合缓冲区。
四、总结
- ByteBuf 本质是一个由不同的索引分别控制读访问和写访问的字节数组。
- ByteBuf 有 readerIndex、writerIndex 和 capacity 三个参数,这三个负责控制 ByteBuf 的读写操作。
- ByteBuf 的内存管理,按照数据的存储位置,可以分为堆缓冲区、直接缓冲区和复合缓冲区;按内存对象是否可以重复利用,可以分为池化内存和非池化内存。
- 创建 ByteBuf 实例,可以使用 ByteBufAllocator 接口或者 Unpooled 工具类。
- ByteBuf 具有引用计数器的概念,当引用计数器为 0 时 ByteBuf 对象会被回收。
五、参考
《Netty In Action》
- Author:mcbilla
- URL:http://mcbilla.com/article/0cfcd0a5-c005-4126-ab8c-9e8c343a0450
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts
