ByteBuf使用详解:ByteBuff组成、扩容、retain、release、零拷贝(slice、duplicate、compositeByteBuf、unpolled)、深拷贝。

        ByteBuf是对字节数据的封装。是netty的Server与Client之间通信的数据传输载体(Netty的数据容器),它提供了一个byte数组(byte[])的抽象视图,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。

创建

        // 创建ByteBuf
        ByteBuf byteBufHeap = ByteBufAllocator.DEFAULT.heapBuffer();
        ByteBuf byteBufDirect = ByteBufAllocator.DEFAULT.directBuffer();
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
        log.info("{}", byteBufHeap.getClass());
        log.info("{}", byteBufDirect.getClass());
        log.info("{}", byteBuf.getClass());

         控制台输出结果如下:

        创建结果:基于池化的堆ByteBuf、基于池化的直接内存ByteBuf、基于池化的直接内存ByteBuf。

 直接内存和堆内存的对比

  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用

  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

参考ByteBuffer中其他优质博客回答:

池化VS非池化

        池化的意义在于可以重用ByteBuf。

  • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力。

  • 有了池化,则可以重用池中 ByteBuf 实例。高并发时,池化功能更节约内存,减少内存溢出的可能。

        可以通过设置虚拟机参数-Dio.netty.allocator.type={unpooled|pooled}来开启或关闭池化技术。Netty4.1之后windows平台默认开启池化技术,关闭池化技术操作如下:

ByteBuf的组成

        ByteBuf主要由四部分组成:废弃字节、可读字节、可写字节、可扩容字节。已经读取过的部门成为废弃字节。

        与ByteBuffer内部只有一个指针控制读写不同(通过读写切换控制读写),ByteBuf内部有一个读指针和一个写指针,分别控制读写的位置。从而省去了读写切换。

         新创建的ByteBuf读写指针均在0位置。

         ByteBuf创建时若不指定字节数,则默认创建字节大写为256;若指定字节数,则依据指定的字节大小创建。ByteBuf能够创建的最大字节数为整型的最大值,约20亿个字节。

ByteBuf的扩容

  • 如果写入后数据大小未超过512,则选择下一个16的整数倍。
  • 如果写入后数据大小超过512,则选择下一个2^n。
  • 且扩容不能超过max capacity。

        例:创建一个ByteBuf初始容量为10,写入后大小为12,则会将其扩容为16。

         例:创建一个ByteBuf初始容量为10,写入后大小为516,则会将其扩容为1024。

ByteBuf的retain和release

由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可

  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存

  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

Netty采用了引用计数法来控制回收内存,每个ByteBuf都实现了ReferenceCounted接口。

  • 初始每个ByteBuf对象的计数为1。
  • 调用release方法计数减1,如果计数为0,ByteBuf内存被回收。release的基本规则:哪个ChannelHandler是最后的使用者,谁负责release。
  • 调用retain方法计数加1,如果调用者没用完之前,其他hander即时调用了release也不会造成回收。
  • 当计数为0时,底层内存会被回收,即时ByteBuf对象还在,其各个方法均无法正常使用。当使用slice及duplicate等操作时需要及时调用retain方法,防止各个方法均无法正常使用。 

ByteBuf中的零拷贝

slice

        slice对原始ByteBuf切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针。

         测试代码如下:用于验证ButeBuf的slice没有发生内存复制。

public class TestSlice {
    public static void main(String[] args) {
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        buf.writeBytes("abcdefghig".toString().getBytes());
        ByteBufUtil.log(buf);
        ByteBuf buf1 = buf.slice(0, 5);
        ByteBuf buf2 = buf.slice(5, 5);
        ByteBufUtil.log(buf1);
        ByteBufUtil.log(buf2);

        System.out.println("=========================");
        // 验证slice切片后的ByteBuf没有发生内存复制,还是使用原始ByteBuf的内存
        buf1.setByte(0, 'g');
        ByteBufUtil.log(buf);
        ByteBufUtil.log(buf1);
    }
}
  • 对原始ByteBuf使用slice形成新的切片后的ByteBuf,其大小受到限制,如果没有指定起始下标以及切片大小,则切片大小为read指针至write指针之间的区域;若指定切片大小,则按照指定大小进行划分。
  • 形成的新的ByteBuf由于容量固定,故不能继续写入元素,只能修改原有下标对应的元素值。
  • 如果释放原有ByteBuf,则切片后形成的ByteBuf也不能使用了。

 

         解决上述问题:切片后对新的ByteBuf使用retain()函数,增加其引用计数,当切片后的ByteBuf使用完毕后及时调用release()方法释放其引用计数。

        修复后代码不再报错,可以正确执行,但需要注意,子ByteBuf调用retain()方法后,不再使用时必须主动调用release()方法,否则原ByteBuf调用release()后无法释放其内存。 

duplicate

        相较于slice()截取原ByteBuf部分内容而言,duplicate是截取了原ByteBuf的所有内容,并且没有max capacity的限制,也是与原始ByteBuf共同使用同一块底层内存,只是读写指针是独立的。

         duplicate使用的是非池化的计数。随着原ByteBuf的release而release。

compositeByteBuf

        compositeByteBuf可以将多个ByteBuf合并为一个ByteBuf,避免了底层内存的拷贝操作。

public class TestCompositeByteBuf {
    public static void main(String[] args) {
        ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.buffer(10);
        ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.buffer(10);
        byteBuf1.writeInt(2).writeInt(4);
        byteBuf2.writeInt(5).writeInt(3);
        // 第二种方式使用compositeByteBuf创建,避免底层数据的复制
        CompositeByteBuf byteBuf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
        byteBuf3.addComponents(true, byteBuf1, byteBuf2);
        ByteBufUtil.log(byteBuf3);

        System.out.println("=================================");
        // 第一种方式创建新的ByteBuf进行数据复制
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(byteBuf1.readableBytes() + byteBuf2.readableBytes());
        byteBuf.writeBytes(byteBuf1).writeBytes(byteBuf2);
        ByteBufUtil.log(byteBuf);
    }
}

        CompositeByteBuf 是一个组合的 ByteBuf,它内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf,记录了这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据。对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制。

Unpooled

        Unpooled是一个工具类,提供了非池化的ByteBuf的创建、组合、复制等操作。其中wrappedBuffer 方法,可以用来包装 ByteBuf。当ByteBuf个数超过一个时,底层使用了compositeByteBuf。

ByteBuf中的深拷贝

        copy会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关。

ByteBuf的优势

  • 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
  • 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
  • 可以自动扩容
  • 支持链式调用,使用更流畅
  • 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf

 

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ByteBufNetty 框架中的一个重要概念,它是一个字节容器,可以方便地进行字节的读写操作。下面就是 ByteBuf 的用法详解。 1. 创建 ByteBuf 创建 ByteBuf 的方式有两种,一种是使用 Unpooled 工具类创建,另一种是使用 ByteBufAllocator 工具类创建。 使用 Unpooled 工具类创建: ```java // 创建一个容量为10个字节的 ByteBuf ByteBuf buf = Unpooled.buffer(10); ``` 使用 ByteBufAllocator 工具类创建: ```java // 获取 ByteBufAllocator 实例 ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; // 创建一个容量为10个字节的 ByteBuf ByteBuf buf = allocator.buffer(10); ``` 2. 写入数据 写入数据可以使用 ByteBuf 的 write 方法,它有很多重载方法,可以写入不同类型的数据。 ```java // 写入一个字节 byte b = 1; buf.writeByte(b); // 写入一个整数 int i = 100; buf.writeInt(i); // 写入一个字符串 String str = "hello world"; buf.writeBytes(str.getBytes()); ``` 3. 读取数据 读取数据可以使用 ByteBuf 的 read 方法,它也有很多重载方法,可以读取不同类型的数据。 ```java // 读取一个字节 byte b = buf.readByte(); // 读取一个整数 int i = buf.readInt(); // 读取一个字符串 byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); String str = new String(bytes); ``` 4. 获取 ByteBuf 的信息 可以使用 ByteBuf 的一些方法获取它的一些信息,比如容量、读索引、写索引等。 ```java // 获取容量 int capacity = buf.capacity(); // 获取可读字节数 int readableBytes = buf.readableBytes(); // 获取可写字节数 int writableBytes = buf.writableBytes(); // 获取读索引 int readerIndex = buf.readerIndex(); // 获取写索引 int writerIndex = buf.writerIndex(); ``` 5. 派生 ByteBuf 可以使用 ByteBufsliceduplicate、readSlice 等方法来派生一个新的 ByteBuf,这个新的 ByteBuf 与原来的 ByteBuf 共享底层的字节数组,但是它们的读写索引是独立的。 ```java // 派生一个新的 ByteBuf,共享底层的字节数组 ByteBuf slice = buf.slice(); // 派生一个新的 ByteBuf,共享底层的字节数组 ByteBuf duplicate = buf.duplicate(); // 派生一个新的 ByteBuf,共享底层的字节数组,但是它的读写索引是独立的 ByteBuf slice = buf.readSlice(5); ``` 6. 释放 ByteBuf使用ByteBuf 后,需要将它释放,否则会出现内存泄漏问题。可以使用 ByteBufrelease 方法来释放它。 ```java buf.release(); ``` 以上就是 ByteBuf 的用法详解,希望对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值