ByteBuf
一. 概述
网络数据的基本单位总是字节,ByteBuf是netty是对字节数据的封装
ByteBuf API 的优点:
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长(类似于 JDK 的 StringBuilder)
- 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
二. 组成
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入
从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增。图 5-1 展示了一个空 ByteBuf 的布局结构和状态。
- 最开始读写指针都在 0 位置
- 只可以读取 readerIndex 和 writerIndex 之间的数据
- 如果读取字节readerIndex 和 writerIndex 同样的值时会触发 IndexOutOfBoundsException。
三. 写入
方法签名 | 含义 | 备注 |
---|---|---|
writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01|00 代表 true|false |
writeByte(int value) | 写入 byte 值 | |
writeShort(int value) | 写入 short 值 | |
writeInt(int value) | 写入 int 值 | Big Endian,即 0x250,写入后 00 00 02 50 |
writeIntLE(int value) | 写入 int 值 | Little Endian,即 0x250,写入后 50 02 00 00 |
writeLong(long value) | 写入 long 值 | |
writeChar(int value) | 写入 char 值 | |
writeFloat(float value) | 写入 float 值 | |
writeDouble(double value) | 写入 double 值 | |
writeBytes(ByteBuf src) | 写入 netty 的 ByteBuf | |
writeBytes(byte[] src) | 写入 byte[] | |
writeBytes(ByteBuffer src) | 写入 nio 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
四. 读取
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(99);
for (int i = 0; i < 100; i++) {
buffer.writeByte(i);
}
System.out.println(buffer.readBoolean()); // 读取当前读索引的Boolean值,读索引加一
System.out.println(buffer.readByte()); // 读取 当前读索引的字节 读索引加一
System.out.println(buffer.readInt()); // 读取当前读索引的int 类型的数据 读索引加四
System.out.println(buffer.readShort()); // 读取当前读索引的 short 类型的数据 读索引加二
System.out.println(buffer);
}
五. ByteBuf 的使用模式
1. Heap Buffer 堆缓冲区 (JVM堆内存)
这是最常用的类型,ByteBuf将数据存储在JVM的堆空间,通过将数据存储在数组中实现的
-
优点是:由于数据存储在JVM的堆中可以快速创建和快速释放,并且提供了数组的直接快速访问的方法。
-
缺点是:每次读写数据都要先将数据拷贝到直接缓冲区再进行传递
使用方式:
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
2 Direct Buffer 直接缓冲区 (直接内存)
NIO 在 JDK 1.4 中引入的 ByteBuffer 类允许 JVM 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地 I/O 操作之前(或者之后)将缓冲区的内容复 制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
Direct Buffer在堆之外直接分配内存,直接缓冲区不会占用堆的容量。事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲 区复制到一个直接缓冲区中。所以如果使用直接缓冲区可以节约一次拷贝
-
优点是:在使用Socket传递数据时性能很好,由于数据直接在内存中,不存在从JVM拷贝数据到直接缓冲区的过程,性能好。
-
缺点是:相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你 正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一 次复制
使用方式:
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
默认也是使用的直接内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
六. 扩容
当写入一个 int 整数时,如果容量不够了,这时会引发扩容
扩容规则:
- 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
- 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 210=1024(29=512 已经不够了)
- 扩容不能超过 max capacity 会报错
七. 池化
池化的最大意义在于可以重用 ByteBuf
- 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
netty 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
八. 内存回收
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
- UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
- UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
- PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
回收内存的源码实现,请关注下面方法的不同实现
protected abstract void deallocate()
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
- 每个 ByteBuf 对象的初始计数为 1
- 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
- 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
- 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用