Netty学习(七)-- ByteBuf

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

猜你喜欢

转载自blog.csdn.net/weixin_43989102/article/details/126736423