目录
1、 ByteBuf
ByteBuf 是对 NIO 中的 ByteBuffer 的增强
1)创建
// 创建 ByteBuf 可以动态扩容 默认初始容量 256
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
以上代码创建了一个默认的 ByteBuf (池化基于直接内存的 ByteBuf),初始容量是 10.
log
方法可使用以下方法:
import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
import static io.netty.util.internal.StringUtil.NEWLINE;
public static void log(ByteBuf buf){
int length = buf.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
StringBuilder sb = new StringBuilder(rows * 80 * 2)
.append("read index: ").append(buf.readerIndex())
.append(" write index: ").append(buf.writerIndex())
.append(" capacity: ").append(buf.capacity())
.append(NEWLINE);
appendPrettyHexDump(sb, buf);
System.out.println(sb);
}
2)直接内存 和 堆内存
创建池化基于堆的 ByteBuf
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();
创建池化基于直接内存的 ByteBuf
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer();
- 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
- 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
3)池化 和 非池化
池化的最大意义在于可以复用 ByteBuf,优点有
- 没有池化,则每次都得创建新的 ByteBuf,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能默认是开启,也可以通过下面的环境变量来设置:
-Dio.netty.allocator.type={unpooled|pooled}
# System.setProperty("io.netty.allocator.type", "pooled");
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化技术还不成熟,默认使用非池化实现
源代码:
static final ByteBufAllocator DEFAULT_ALLOCATOR;
static {
String allocType = SystemPropertyUtil.get(
"io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
4)组成
ByteBuf 由四部分组成
最开始读写指针都在 0 位置
5)写入
方法名 | 含义 | 备注 |
---|---|---|
writeBoolean(boolean value) | 写入 boolean 值 | 用一个字节 `01 |
writeInt(int value) | 写入 Int 值 | Big Endian(大端写入),即 0x250 ,写入后 00 00 02 50 |
writeIntLE(int value) | 写入 Int 值 | Little Endian(小端写入), 即 0x205 ,写入后 50 02 00 00 |
writeBytes(ByteBuffer src) | 写入 NIO 的 ByteBuffer | |
writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
注意:
- 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用
- 网络传输,默认习惯是大端写入
6)扩容
扩容规则是
-
如果写入后数据大小未超过
512
,则选择下一个16
的整数倍,例如写入后大小为10
, 则扩容后 capacity 是16
-
如果写入后数据大小超过了
512
,则选择下一个2^n
,例如写入后大小为513
, 则扩容后 capacity 是2^10 = 1024 (2^9=512不够)
-
扩容不能超过
max capacity
会报错
7)读取
get
开头的一系列方法,这些方法不会改变read index
// 重复读取,现在读取之前做个标记 mark
buf.markReaderIndex();
// 这时需要重读读取的话,读指针重置到标记位置
buf.resetReaderIndex();
8)retain & relese
由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。
UnpooledHeapByteBuf
使用的是 JVM 内存,只需等 GC 回收内存即可。UnpooledDirectByteBuf
使用的就是直接内存了,需要特殊的方法来回收内存。PooledByteBuf<T>
和 它的子类使用了池化机制,需要更复杂的规则来回收内存。
回收内存的源码实现,请关注下面方法的不同实现
AbstractReferenceCountedByteBuf
:protected abstract void deallocate();
Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都是实现了 ReferenceCounted
接口
- 每个 ByteBuf 对象的初始计数为 1。
- 调用
release()
方法计数减 1 ,如果计数为 0,ByteBuf 内存被回收。 - 调用
retain()
方法计数加 1,表示调用者没用完之前,其他 handler 即时调用release()
方法也不会造成回收。 - 当计数为 0 时,底层内存会被回收,这时即时 ByteBuf 对象还在,其他各方法均无法正常使用。
谁来负责 release
?
一般情况下:
Byteuf buf = ...;
try{
...
}finally{
buf.release();
}
ByteBuf 传到头尾释放的话,会由头尾释放,但在中间的handler,需要自己处理。
因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么无须再传递)
基本规则是,谁是最后使用者,谁来负责 release,
分析:(源码)
9)slice
前面的零拷贝指的是由文件 Channel 传输数据的时候,可以不经过 Java 内存直接从文件走到 socket 网络设备,减少了数据的复制
【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf ,切片后的 ByteBuf 并没有发生内存复制,还是使用原始的 ByteBuf 内存,切片后的 ByteBuf 维护独立的 read、write指针。
代码测试
public class TestSlice {
public static void main(String[] args) {
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
byteBuf.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'});
log(byteBuf);
// 在切片的过程中没有发生数据的复制
ByteBuf buf1 = byteBuf.slice(0, 5);
ByteBuf buf2 = byteBuf.slice(5, 5);
log(buf1);
log(buf2);
// 测试是否是同一块内存
buf1.setByte(0, 'x');
log(byteBuf);
log(buf1);
// 切片后,最大容量做了限制 IndexOutOfBoundsException
buf1.writeByte('s');
// 释放原有的 ByteBuf, buf1、buf2都会释放掉,可以使用
buf1.retain();
buf2.retain();
byteBuf.release();
log(buf1);
// 注意:用完之后需要自己释放
buf1.release();
buf2.release();
}
}
10)duplicate
【零拷贝】的体现之一,就好比截取了原始的 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,知识读写指针是独立的。
11)copy
会将底层内存数据进行深拷贝 ,因此无论读写,都与原始 ByteBuf 无关
12)composite
【零拷贝】将多个 ByteBuf 组成一个 ByteBuf。
private static void testComposite() {
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer();
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
buf1.writeBytes(new byte[]{6, 7, 8, 9, 10});
// 传统的
// ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// buffer.writeBytes(buf1).writeBytes(buf2);
// log(buffer);
// composite
CompositeByteBuf compositeBuffer = ByteBufAllocator.DEFAULT.compositeBuffer();
compositeBuffer.addComponents(true, buf1, buf2);
log(compositeBuffer);
}
13)Unpooled
Unpooled
是一个工具类,类如其名,提供了非池化的 ByteBuf 创建、组合、复制等操作。
【零拷贝】相关的 wrappedBuffer()
方法。
private static void testUnpooled() {
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer();
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
buf1.writeBytes(new byte[]{6, 7, 8, 9, 10});
// 当包装 ByteBuf 个数超过一个时,底层使用了 CompositeByteBuf
ByteBuf byteBuf = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(byteBuf.getClass());
log(byteBuf);
}
ByteBuf 的优势
- 池化:可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要向 ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用
- 很多地方体现零拷贝,如 slice, duplicate, CompositeByteBuf