ByteBuf 和 ByteBuffer 的区别, ByteBuf 动态扩容源码分析

2,448 阅读2分钟

「我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

在我们进行数据传输的时候, 往往需要使用到缓冲区, 在 Java NIO 中提供的缓冲区类是ByteBuffer, 在Netty框架中提供的缓冲区类是ByteBuf, 接下来我们就针对这两种缓冲区进行对比讲解

往期文章

ByteBuffer

关于ByteBuffer我之前专门的写过一篇文章讲解概念, 基本使用和方法等

NIO 下的 ByteBuffer简单学习 - 掘金 (juejin.cn)

缺点

  • 长度固定: 一旦对ByteBuffer进行分配完成, 他的容量不能动态扩展和收缩, 当需要编码的对象大于ByteBuffer的容量时, 会发生索引越界异常
  • API功能相对较少: 对一些高级和实用的特性不支持, 需要自己进行对应的封装
  • 只有一个指针position: 读写的时候需要手动的调用rewind()方法和flip()方法

ByteBuf

NettyByteBuf弥补了ByteBuffer的不足之处, 两者是在同等层面, 都是用于缓冲区的

优势

  • 支持池化: 更节约内存, 减少内存溢出的可能
  • 读写指针分离, 不需要像ByteBuffer一样需要调用方法来切换读写模式
  • 可以自动扩容, 有效解决了ByteBuffer的索引越界异常
  • 支持方法的链式调用
  • 很多地方体现了零拷贝

动态扩容机制

代码体验一下自动扩容机制

工具类

public class ByteBufUtil {

    public static void log(ByteBuf buf){
        final int length = buf.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder str = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buf.readerIndex())
                .append(" write index: ").append(buf.writerIndex())
                .append(" capacity:").append(buf.capacity());
        appendPrettyHexDump(str, buf);
        System.out.println(str.toString());
    }

}

启动类

public static void main(String[] args) {
    // 创建 ByteBuf, 初始化长度为 16
    ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(16);
    ByteBufUtil.log(byteBuf);

    // 向 byteBuf 缓冲区写入数据
    StringBuilder str = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        str.append("nx");
    }
    byteBuf.writeBytes(str.toString().getBytes());

    // 打印当前 byteBug
    ByteBufUtil.log(byteBuf);
}

打印结果

image.png

根据我们的启动类代码可以看到, 我们的ByteBuf初始长度只有16, 后面我们为其添加了20字节的元素, ByteBuf的容量被自动的扩容到了64

判断是否动态扩容

首先我们在写入方法ByteBuf.writeBytes()处打断点, 通过debug模式进入该方法中

image.png

然后我们会进入到AbstractByteBuf类的writeBytes()方法, 方法重写, 进入上面那个真正的方法

在该方法中, 第912行, this.ensureWritable()方法是对入参长度和当前ByteBuf容量进行扩容的方法, 我们点进去看一下详情

image.png

然后会进入下面的这个方法, 我主要讲解一下ensureWritable0()方法的流程:

  • 获取当前的写入下标
  • 获取目标容量
  • 如果目标容量大于等于0 同时目标容量比capacity
    • 则直接返回
  • 如果checkBoundstrue 同时 (目标容量小于0 或者 目标容量大于maxCapacity)
    • maxCapacity = 2147483647
    • 目标容量不合理 , 报错
  • 如果上面两个都不是, 则进入 else
    • 获取初始化时设置的容量大小 (我们设置的16)
    • 如果初始容量大于等于目标容量
      • 则进行想加操作
      • 否则, 进行扩容操作( this.alloc().calculateNewCapacity()方法 )
      • 执行capacity(newCapacity)方法

image.png

动态扩容核心方法

接下来我们进入到AbstractByteBufAllocator类的calculateNewCapacity()方法中, 该方法是ByteBuf动态扩容的核心方法

image.png

在这个方法中, 首先去判断目标容量是否比最大容量还大, 如果目标容量比最大容量还大的话就抛出异常, 实际上这一步在之前我们也见到过了

然后我们为当前ByteBuf的容量capacity设置了一个阈值threshold = 4194304大概 4M 的样子

然后又采用了一个步进4MB的方式进行扩容操作

image.png

如果目标大小没有达到阈值的话, 以64为基数, 做倍增的方式进行扩容

image.png




本文内容到此结束了

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

我是 宁轩 , 我们下次再见