ByteBuf 和 ByteBuffer的比对

简介: ByteBuf 和 ByteBuffer的比对

最近在写一些中间件底层的模块,经常需要和netty相关的内容打交道,偶尔会回顾到一些关于nio的知识点,今天正好有空,抽空整理了一些关于nio和netty经常要用到的两个类:ByteBuff 和 ByteBuffer。


ByteBuffer



ByteBuffer是属于nio的一款字节缓冲区类,属于原生jdk内置的。常用到的方式为:


1.首先创建基本对象

2.调用filp方法切换为读模式

3.读取之前写入的数据

4.调用clear或者compact进行缓冲区的数据清除工作


代码案例:


public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(20);
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        byteBuffer.put((byte) 4);
        //1
        byteBuffer.flip();
        System.out.println(Arrays.toString(byteBuffer.array()));
        byteBuffer.put((byte) 0);
        System.out.println(Arrays.toString(byteBuffer.array()));
        //2
        byteBuffer.compact();
        byteBuffer.put((byte) 5);
        byteBuffer.put((byte) 6);
        System.out.println(Arrays.toString(byteBuffer.array()));
    }    
复制代码


最终打印出来的结果为:


[1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
复制代码


这里解释一下1和2处的差异点:


1:调用了flip函数之后,程序自身会切换为读模式,position指针会切换到第一位0,因此此时bytebuffer内部的存储数据是正常的1,2,3,4,5。


简单点来理解“读模式”,就是通过变换position指针来实现读取byte数组中未读取过的数据信息。byteBuffer.get()可以按照数组下标的顺序依次获取到byte数组中的每个元素。


2: compact函数的调用其实就是将已经读过的元素给清空,然后从未写入的位置开始继续写入数据。


ByteBuffer底层结构



关于ByteBuffer的底层结构部分,主要需要关心以下几个要点:


position, limit, capactiy
复制代码


这三个关键字属性分别代表的含义可以通过下方程序来查看:


public static void showByteBuffer(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(5);
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        //1
        print(byteBuffer);
        //2
        byteBuffer.flip();
        print(byteBuffer);
        //3
        byteBuffer.get();
        print(byteBuffer);
        //4
        byteBuffer.compact();
        print(byteBuffer);
        //5
        byteBuffer.clear();
        print(byteBuffer);
    }
    public static void print(ByteBuffer byteBuffer){
        System.out.println("byteBuffer limit is:"+byteBuffer.limit()+" position is:"+byteBuffer.position()+" capacity is:"+byteBuffer.capacity()+" bytes[] is :"+ Arrays.toString(byteBuffer.array()));
    }
复制代码


程序执行之后打印的结果为:


byteBuffer limit is:5 position is:3 capacity is:5 bytes[] is :[1, 2, 3, 0, 0]
byteBuffer limit is:3 position is:0 capacity is:5 bytes[] is :[1, 2, 3, 0, 0]
byteBuffer limit is:3 position is:1 capacity is:5 bytes[] is :[1, 2, 3, 0, 0]
byteBuffer limit is:5 position is:2 capacity is:5 bytes[] is :[2, 3, 3, 0, 0]
byteBuffer limit is:5 position is:0 capacity is:5 bytes[] is :[2, 3, 3, 0, 0]
复制代码


这里我画几张图来解释一下bytebuffer对于数据存储时候的处理机制:


首先来看到1号程序执行的位置:


代码里面往byteBuffer中put了三个数值,分别是1,2,3。而byteBuffer实际上存储的时候是将数据存储为了byte[]数组的格式,此时position会在每一次put操作之后自增+1操作,因此此时position位于第四个索引下标,打印的时候也就显示为3。而capacity和limit两个属性则是在一开始初始化操作的时候就定义好了的,目前依旧保持为5不变。


网络异常,图片无法展示
|


接着是来看到2号程序位:


由于byteBuffer切换为了读模式,flip之后position会发生变动,并且limit也会做调整。


网络异常,图片无法展示
|


接着是再看看3号程序位:


光是调整为了读模式还不足,在触发了get函数之后(可以理解为就是调用了一次读取字节内存一个数组的含义),position会在get之后发生自增,因此此时的byteBuffer结果如下:


网络异常,图片无法展示
|


4号程序位模块:


执行了一次compact函数之后,byteBuffer中会在进行一轮模式的切换,变为写模式,此时会将已经读取过的数据给清空。其实本质就是通过移动数据内部的数据,然后从新的位置写入:

网络异常,图片无法展示
|


5号程序位执行分析:


网上有些资料讲解说clear函数和compact函数的左右是相似的,但是其实本质上还是有些差异。clear函数也是会对byteBuffer内部进行一轮清空操作,但是只是将position的位置做挪动,调整到0的下标位置,内部存储的元素数值并不会做调整。而调用compact则会将已读的数据进行覆盖。


网络异常,图片无法展示
|


ByteBuffer不足之处


在对byteBuffer内部构造有了一定原理了解之后,我们可以分析得出它相关的不足之处:

1.内存分配需要预先得知,如果内存不足会在写入到达阈值的时候出现异常( java.nio.BufferOverflowException )。


2.对于各种filp,clear,compact之间的切换,如果不了解底层机制,很容易出现异常,灵活性不足。


针对这些问题,netty框架中提出了一款ByteBuff进行了二次封装,将其进行了优化开发,下边我们来看看这么一段代码:


public static void main(String[] args) {
        //DirectByteBuffer 使用堆外内存
        ByteBuf byteBuf = Unpooled.buffer(10);
        byteBuf.writeByte(1);
        byteBuf.writeByte(2);
        byteBuf.writeByte(3);
        byteBuf.writeByte(4);
        byteBuf.writeByte(5);
        printByteBuf(byteBuf);
        System.out.println(byteBuf.readByte());
        System.out.println(byteBuf.readByte());
        printByteBuf(byteBuf);
    }
    private static void printByteBuf(ByteBuf byteBuf){
        System.out.println("byteBuf is :"+ Arrays.toString(byteBuf.array())+" rix is :"+byteBuf.readerIndex()+" wix is :"+byteBuf.writerIndex());
    }
复制代码


相关的程序结果为:


byteBuf is :[1, 2, 3, 4, 5, 0, 0, 0, 0, 0] rix is :0 wix is :5
1
2
byteBuf is :[1, 2, 3, 4, 5, 0, 0, 0, 0, 0] rix is :2 wix is :5
复制代码


在netty封装的ByteBuf里面,包含了两个特殊的指针,分别是writeIndex和readIndex,这两个属性可以帮助我们进行内存的动态扩张,同时也完善了ByteBuffer内部繁琐的模式切换问题。


堆外缓存


在java程序当中,我们比较常见到的就是使用堆来进行内存存储,但是在nio出现之后,ByteBuffer提供了堆外内存分配的机制帮助我们实现对外内存的存储。


先来解释一下几个名词:


堆内存:也就是我们常常说的jvm内部的堆内存模块,这部分的内存是自带有回收机制的,当内存不足的时候会自动进行gc回收,从而释放掉不需要的内存空间。


堆外内存:该模块的内存都没有受到jvm的垃圾回收进行管理,由于不受到gc的影响,堆

外内存有了一个比较常见的落地场景—缓存。JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。


堆外内存的代码案例


public static void test(){
        //分配128MB直接内存
        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("ok");
    }
复制代码


这段代码在启动的时候可以尝试加入以下参数,并且进行参数值的灵活调整。


-Xmx100m 
-XX:MaxDirectMemorySize=150M
复制代码


java应用在本身运行的时候,如果没有设置MaxDirectMemorySize参数,那么就会将该参数设置为和-Xmx一致。如果堆外内存的分配过多,超过了设置阈值,就会抛出java.lang.OutOfMemoryError: Direct buffer memory的异常信息。


ps:熟悉kafka的小伙伴应该会有接触过类似的异常问题排查。


堆外缓存的落地


有一个叫做Ehcache的缓存组件就有使用过堆外内存进行实现,感兴趣的小伙伴可以后边研究一下。

目录
相关文章
2. ByteBuffer
2. ByteBuffer
35 0
|
3月前
|
存储
victoriaMetrics之byteBuffer
victoriaMetrics之byteBuffer
43 2
|
3月前
|
缓存 Java 索引
ByteBuffer 字节缓冲区
ByteBuffer 字节缓冲区
38 0
2.1 ByteBuffer 正确使用姿势
2.1 ByteBuffer 正确使用姿势
62 0
|
6月前
|
Java API 索引
Netty Review - ByteBuf 读写索引 详解
Netty Review - ByteBuf 读写索引 详解
188 1
|
索引
2.3 ByteBuffer 常见方法
2.3 ByteBuffer 常见方法
65 0
|
存储
ByteBuffer 大小分配
ByteBuffer 大小分配
86 0
|
存储 Java 关系型数据库
【Java I/O 流】字节数组流:ByteArrayInputStream 和 ByteArrayOutputStream
今天的主题是字节数组流,即`ByteArrayInputStream` 和 `ByteArrayOutputStream`。这两个流主要用于字节数组与流之前的转换。
281 0
|
算法 Java 索引
ByteBuffer
ByteBuffer
80 0
|
存储 消息中间件 缓存
ByteBuffer总结
ByteBuffer总结
124 0