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的缓存组件就有使用过堆外内存进行实现,感兴趣的小伙伴可以后边研究一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值