乐趣区

关于java:BAT面试必问细节关于Netty中的ByteBuf详解

在 Netty 中,还有另外一个比拟常见的对象 ByteBuf,它其实等同于 Java Nio 中的 ByteBuffer,然而 ByteBuf 对 Nio 中的 ByteBuffer 的性能做了很作加强,上面咱们来简略理解一下 ByteBuf。

上面这段代码演示了 ByteBuf 的创立以及内容的打印,这里显示出了和一般 ByteBuffer 最大的区别之一,就是 ByteBuf 能够主动扩容,默认长度是 256,如果内容长度超过阈值时,会主动触发扩容

public class ByteBufExample {public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();// 可主动扩容
        log(buf);
        StringBuilder sb=new StringBuilder();
        for (int i = 0; i < 32; i++) {  // 演示的时候,能够把循环的值扩充,就能看到扩容成果
            sb.append("-"+i);
        }
        buf.writeBytes(sb.toString().getBytes());
        log(buf);
    }

    private static void log(ByteBuf buf){StringBuilder builder=new StringBuilder()
            .append("read index:").append(buf.readerIndex()) // 获取读索引
            .append("write index:").append(buf.writerIndex()) // 获取写索引
            .append("capacity:").append(buf.capacity()) // 获取容量
            .append(StringUtil.NEWLINE);
        // 把 ByteBuf 中的内容,dump 到 StringBuilder 中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

ByteBuf 创立的办法有两种

  • 第一种,创立基于堆内存的 ByteBuf

    ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10);
  • 第二种,创立基于间接内存(堆外内存)的 ByteBuf(默认状况下用的是这种

    Java 中的内存分为两个局部,一部分是不须要 jvm 治理的间接内存,也被称为堆外内存。堆外内存就是把内存对象调配在 JVM 堆意外的内存区域,这部分内存不是虚拟机治理,而是由操作系统来治理,这样能够缩小垃圾回收对应用程序的影响

    ByteBufAllocator.DEFAULT.directBuffer(10);

    间接内存的益处是读写性能会高一些,如果数据寄存在堆中,此时须要把 Java 堆空间的数据发送到近程服务器,首先须要把堆外部的数据拷贝到间接内存(堆外内存),而后再发送。如果是把数据间接存储到堆外内存中,发送的时候就少了一个复制步骤。

    然而它也有毛病,因为短少了 JVM 的内存治理,所以须要咱们本人来保护堆外内存,避免内存溢出。

另外,须要留神的是,ByteBuf 默认采纳了池化技术来创立。对于池化技术在后面的课程中曾经反复讲过,它的核心思想是实现对象的复用,从而缩小对象频繁创立销毁带来的性能开销。

池化性能是否开启,能够通过上面的环境变量来管制,其中 unpooled 示意不开启。

-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample {public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
        System.out.println(buf);
    }
}

ByteBuf 的存储构造

ByteBuf 的存储构造如图 3 - 1 所示,从这个图中能够看到 ByteBuf 其实是一个字节容器,该容器中蕴含三个局部

  • 曾经抛弃的字节,这部分数据是有效的
  • 可读字节,这部分数据是 ByteBuf 的主体数据,从 ByteBuf 外面读取的数据都来自这部分;可写字节,所有写到 ByteBuf 的数据都会存储到这一段
  • 可扩容字节,示意 ByteBuf 最多还能扩容多少容量。

<center> 图 3 -1</center>

在 ByteBuf 中,有两个指针

  • readerIndex:读指针,每读取一个字节,readerIndex 自减少 1。ByteBuf 外面总共有 witeIndex-readerIndex 个字节可读,当 readerIndex 和 writeIndex 相等的时候,ByteBuf 不可读
  • writeIndex:写指针,每写入一个字节,writeIndex 自减少 1,直到减少到 capacity 后,能够触发扩容后持续写入。
  • ByteBuf 中还有一个 maxCapacity 最大容量,默认的值是Integer.MAX_VALUE,当 ByteBuf 写入数据时,如果容量有余时,会触发扩容,直到 capacity 扩容到 maxCapacity。

ByteBuf 中罕用的办法

对于 ByteBuf 来说,常见的办法就是写入和读取

Write 相干办法

对于 write 办法来说,ByteBuf 提供了针对各种不同数据类型的写入,比方

  • writeChar,写入 char 类型
  • writeInt,写入 int 类型
  • writeFloat,写入 float 类型
  • writeBytes,写入 nio 的 ByteBuffer
  • writeCharSequence,写入字符串
public class ByteBufExample {public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();// 可主动扩容
        buf.writeBytes(new byte[]{1,2,3,4}); // 写入四个字节
        log(buf);  
        buf.writeInt(5);  // 写入一个 int 类型,也是 4 个字节
        log(buf);
    }
    private static void log(ByteBuf buf){System.out.println(buf);
        StringBuilder builder=new StringBuilder()
                .append("read index:").append(buf.readerIndex())
                .append("write index:").append(buf.writerIndex())
                .append("capacity:").append(buf.capacity())
                .append(StringUtil.NEWLINE);
        // 把 ByteBuf 中的内容,dump 到 StringBuilder 中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

扩容

当向 ByteBuf 写入数据时,发现容量有余时,会触发扩容,而具体的扩容规定是

假如 ByteBuf 初始容量是 10。

  • 如果写入后数据大小未超过 512 个字节,则抉择下一个 16 的整数倍进行库容。比方写入数据后大小为 12,则扩容后的 capacity 是 16。
  • 如果写入后数据大小超过 512 个字节,则抉择下一个 2^n^。比方写入后大小是 512 字节,则扩容后的 capacity 是 2^10^=1024。(因为 2^9^=512,长度曾经不够了)
  • 扩容不能超过 max capacity,否则会报错。

Reader 相干办法

reader 办法也同样针对不同数据类型提供了不同的操作方法,

  • readByte,读取单个字节
  • readInt,读取一个 int 类型
  • readFloat,读取一个 float 类型
public class ByteBufExample {public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();// 可主动扩容
        buf.writeBytes(new byte[]{1,2,3,4});
        log(buf);
        System.out.println(buf.readByte());
        log(buf);
    }
    private static void log(ByteBuf buf){StringBuilder builder=new StringBuilder()
            .append("read index:").append(buf.readerIndex())
            .append("write index:").append(buf.writerIndex())
            .append("capacity:").append(buf.capacity())
            .append(StringUtil.NEWLINE);
        // 把 ByteBuf 中的内容,dump 到 StringBuilder 中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

从上面后果中能够看到,读完一个字节后,这个字节就变成了废除局部,再次读取的时候只能读取 未读取的局部数据。

read index:0 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07                            |.......         |
+--------+-------------------------------------------------+----------------+
1
 read index:1 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 02 03 04 05 06 07                               |......          |
+--------+-------------------------------------------------+----------------+

Process finished with exit code 0

另外,如果想反复读取哪些曾经读完的数据,这里提供了两个办法来实现标记和重置

public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();// 可主动扩容
    buf.writeBytes(new byte[]{1,2,3,4,5,6,7});
    log(buf);
    buf.markReaderIndex(); // 标记读取的索引地位
    System.out.println(buf.readInt());
    log(buf);
    buf.resetReaderIndex();// 重置到标记位
    System.out.println(buf.readInt());
    log(buf);
}

另外,如果想不扭转读指针地位来取得数据,在 ByteBuf 中提供了 get 结尾的办法,这个办法基于索引地位读取,并且容许反复读取的性能。

ByteBuf 的零拷贝机制

须要阐明一下,ByteBuf 的零拷贝机制和咱们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是咱们要把一个文件发送到近程服务器时,须要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数减少。

而 ByteBuf 中的零拷贝思维也是雷同,都是缩小数据复制晋升性能。如图 3 - 2 所示,假如有一个原始 ByteBuf,咱们想对这个 ByteBuf 其中的两个局部的数据进行操作。依照失常的思路,咱们会创立两个新的 ByteBuf,而后把原始 ByteBuf 中的局部数据拷贝到两个新的 ByteBuf 中,然而这种会波及到数据拷贝,在并发量较大的状况下,会影响到性能。

<center> 图 3 -2</center>

ByteBuf 中提供了一个 slice 办法,这个办法能够在不做数据拷贝的状况下对原始 ByteBuf 进行拆分,应用办法如下

public static void main(String[] args) {ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();// 可主动扩容
    buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10});
    log(buf);
    ByteBuf bb1=buf.slice(0,5);
    ByteBuf bb2=buf.slice(5,5);
    log(bb1);
    log(bb2);
    System.out.println("批改原始数据");
    buf.setByte(2, 5); // 批改原始 buf 数据
    log(bb1);// 再打印 bb1 的后果,发现数据产生了变动
}

在下面的代码中,通过 slice 对原始 buf 进行切片,每个分片是 5 个字节。

为了证实 slice 是没有数据拷贝,咱们通过批改原始 buf 的索引 2 所在的值,而后再打印第一个分片 bb1,能够发现 bb1 的后果产生了变动。阐明两个分片和原始 buf 指向的数据是同一个。

Unpooled

在后面的案例中咱们常常用到 Unpooled 工具类,它是同了非池化的 ByteBuf 的创立、组合、复制等操作。

假如有一个协定数据,它有头部和音讯体组成,这两个局部别离放在两个 ByteBuf 中

ByteBuf header=...
ByteBuf body= ...

咱们心愿把 header 和 body 合并成一个 ByteBuf,通常的做法是

ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

在这个过程中,咱们把 header 和 body 拷贝到了新的 allBuf 中,这个过程在无形中减少了两次数据拷贝操作。那有没有更高效的办法缩小拷贝次数来达到雷同目标呢?

在 Netty 中,提供了一个 CompositeByteBuf 组件,它提供了这个性能。

public class ByteBufExample {public static void main(String[] args) {ByteBuf header= ByteBufAllocator.DEFAULT.buffer();// 可主动扩容
        header.writeCharSequence("header", CharsetUtil.UTF_8);
        ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
        body.writeCharSequence("body", CharsetUtil.UTF_8);
        CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
        // 其中第一个参数是 true, 示意当增加新的 ByteBuf 时, 主动递增 CompositeByteBuf 的 writeIndex.
        // 默认是 false,也就是 writeIndex=0,这样的话咱们不可能从 compositeByteBuf 中读取到数据。compositeByteBuf.addComponents(true,header,body);
        log(compositeByteBuf);
    }
    private static void log(ByteBuf buf){StringBuilder builder=new StringBuilder()
            .append("read index:").append(buf.readerIndex())
            .append("write index:").append(buf.writerIndex())
            .append("capacity:").append(buf.capacity())
            .append(StringUtil.NEWLINE);
        // 把 ByteBuf 中的内容,dump 到 StringBuilder 中
        ByteBufUtil.appendPrettyHexDump(builder,buf);
        System.out.println(builder.toString());
    }
}

之所以 CompositeByteBuf 可能实现零拷贝,是因为在组合 header 和 body 时,并没有对这两个数据进行复制,而是通过 CompositeByteBuf 构建了一个逻辑整体,外面依然是两个实在对象,也就是有一个指针指向了同一个对象,所以这里相似于浅拷贝的实现。

wrappedBuffer

在 Unpooled 工具类中,提供了一个 wrappedBuffer 办法,来实现 CompositeByteBuf 零拷贝性能。应用办法如下。

public static void main(String[] args) {ByteBuf header= ByteBufAllocator.DEFAULT.buffer();// 可主动扩容
    header.writeCharSequence("header", CharsetUtil.UTF_8);
    ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
    body.writeCharSequence("body", CharsetUtil.UTF_8);
    ByteBuf allBb=Unpooled.wrappedBuffer(header,body);
    log(allBb);
    // 对于零拷贝机制,批改原始 ByteBuf 中的值,会影响到 allBb
    header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);
    log(allBb); 
}

copiedBuffer

copiedBuffer,和 wrappedBuffer 最大的区别是,该办法会实现数据复制,上面代码演示了 copiedBuffer 和 wrappedbuffer 的区别,能够看到在 case 标注的地位中,批改了原始 ByteBuf 的值,并没有影响到 allBb。

public static void main(String[] args) {ByteBuf header= ByteBufAllocator.DEFAULT.buffer();// 可主动扩容
    header.writeCharSequence("header", CharsetUtil.UTF_8);
    ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
    body.writeCharSequence("body", CharsetUtil.UTF_8);
    ByteBuf allBb=Unpooled.copiedBuffer(header,body);
    log(allBb);
    header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case
    log(allBb);
}

内存开释

针对不同的 ByteBuf 创立,内存开释的办法不同。

  • UnpooledHeapByteBuf,应用 JVM 内存,只须要期待 GC 回收即可
  • UnpooledDirectByteBuf,应用对外内存,须要非凡办法来回收内存
  • PooledByteBuf 和它的之类应用了池化机制,须要更简单的规定来回收内存

如果 ByteBuf 是应用堆外内存来创立,那么尽量手动开释内存,那怎么开释呢?

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

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 办法时,计数器减一,如果计数器为 0,ByteBuf 被回收
  • 调用 retain 办法时,计数器加一,示意调用者没用完之前,其余 handler 即时调用了 release 也不会造成回收。
  • 当计数器为 0 时,底层内存会被回收,这时即便 ByteBuf 对象还存在,然而它的各个办法都无奈失常应用

版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自 Mic 带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!

退出移动版