byteBuffer_bytebuffer flip

byteBuffer_bytebuffer flip为什么会在RocketMQ系列里面参杂一篇ByteBuffer的文章呢?因为RocketMQ存储消息,是存储在文件中的,而且刚好使用的是ByteBuffer。这个属于JavaNIO的内容,用到的比较少,如果像我一样没有相关的知识做铺垫,强行看RocketMQ消息存储相关的代码会比较头疼。为了减少学习难度,这里很有必要先介绍一下ByteBuffer相关的知识。…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

为什么会在RocketMQ系列里面参杂一篇ByteBuffer的文章呢?因为RocketMQ存储消息,是存储在文件中的,而且刚好使用的是ByteBuffer。这个属于Java NIO的内容,平时用到的非常少,如果像我一样没有相关的知识做铺垫,强行看RocketMQ消息存储相关的代码会比较头疼。为了减少学习难度,这里很有必要先介绍一下ByteBuffer相关的知识。

Buffer就是缓冲区的意思。如果数据直接来了就写入磁盘,那么肯定I/O操作太频繁,效率上不去。如果先写入缓冲区,等缓冲区数据量够了再一次性写入磁盘,是不是就好多了。

ByteBuffer就是存放字节的缓冲区。ByteBuffer继承了Buffer类,主要属性如下所示:

    private int mark = -1; // 标记位,配合mark()和reset()方法使用,可以记录某一次访问的位置。
    private int position = 0; // 访问位置,每访问一个byte就+1
    private int limit; // 读写限制位置,ByteBuffer的读写都不能超过这个位置
    private int capacity; // 容量,初始化后就不会再变了
    
    long address; // 访问地址,仅仅针对DirectByteBuffer,使用的是堆外内存。
    
    final byte[] hb; // 堆内内存时存放的内容
    final int offset; // 偏移量,使用byte数组初始化ByteBuffer时,数组的偏移量
    boolean isReadOnly; // 是否只读

上面的属性没有完全理解,也没有关系,后面会再次涉及到的,有个初步印象即可。

初始化

虽然ByteBuffer有构造函数,但是我们一般不用,转而使用ByteBuffer下面的几个静态函数来构造ByteBuffer对象。

    /** * 分配一个使用堆外内存的ByteBuffer */
    public static ByteBuffer allocateDirect(int capacity) { 
   
        return new DirectByteBuffer(capacity);
    }

   /**分配一个堆内内存的ByteBuffer*/
    public static ByteBuffer allocate(int capacity) { 
   
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

   /**把给定的byte数组包装成一个byteBuffer*/
    public static ByteBuffer wrap(byte[] array,
                                    int offset, int length)
    { 
   
        try { 
   
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) { 
   
            throw new IndexOutOfBoundsException();
        }
    }

    /**同上*/
    public static ByteBuffer wrap(byte[] array) { 
   
        return wrap(array, 0, array.length);
    }

例如:

        final int CAPACITY = 1024 * 1024 * 50;
        ByteBuffer byteBufferHeap = ByteBuffer.allocate(CAPACITY);
        ByteBuffer byteBufferDirect = ByteBuffer.allocateDirect(CAPACITY);
        
        byte[] array = new byte[]{ 
   1,2,3};
        ByteBuffer byteBuffer = ByteBuffer.wrap(array);

基本读写操作

下面是Thinking in java中关于缓冲器(ByteBuffer,又叫缓冲区)和通道(Channel)之间关系的描述:
在这里插入图片描述
从上述描述中,我们大致可以认为文件资源可以转换成一个通道,然后通过ByteBuffer读写文件。下面是一个简单的文件读取到byteBuffer的例子:

public static void main(String[] args) throws Exception { 
   

        String path = "d:" + File.separator + "bill"; // 文件内容是"abcdefg1234567890"
        FileInputStream fileInputStream = new FileInputStream(path);
        FileChannel fileChannel = fileInputStream.getChannel(); // 我们可以通过FileInputStream和FileOutPutStream提供FileChannel对象。

        final int CAPACITY = 20; // 容量20个字节
        ByteBuffer byteBufferHeap = ByteBuffer.allocate(CAPACITY);

        fileChannel.read(byteBufferHeap); // 读取文件到缓冲区
 
        System.out.println(byteBufferHeap.position()); // 结果是17,读一个byte,position+1
        System.out.println(byteBufferHeap.capacity()); // 结果是20
        System.out.println(byteBufferHeap.limit()); // 结果是20

        byteBufferHeap.flip();// flip(翻转)操作会将当前position记录到limit中, 然后position翻转指回0,mark置为-1.
        // byteBufferHeap.clear(); // 后面会讲解
        
        System.out.println(byteBufferHeap.position()); // 结果是0
        System.out.println(byteBufferHeap.capacity()); // 容量不变,还是20
        System.out.println(byteBufferHeap.limit()); // 结果17
        
        while(byteBufferHeap.hasRemaining()){ 
    // 如果Postion和limit之间还有元素
            System.out.print((char)byteBufferHeap.get());
        }

        fileChannel.close();
    }

下面来一个简单的byteBuffer写文件例子:

public static void main(String[] args) throws Exception{ 
   
        String content = "我是内容";
        ByteBuffer byteBufferHeap = ByteBuffer.wrap(content.getBytes("UTF-8")); // 通过warp创建ByteBuffer

        String path = "d:" + File.separator + "out.txt";
        FileChannel fileChannel = new FileOutputStream(path).getChannel();
        fileChannel.write(byteBufferHeap); // 把ByteBuffer内容写入channel

        String append = "我是追加内容";
        fileChannel.position(fileChannel.size());// 移动到文件尾
        fileChannel.write(ByteBuffer.wrap(append.getBytes("UTF-8")));
        
        fileChannel.close();
    }

flip()和clear()

flip(翻转)操作会将当前position记录到limit中, 然后position翻转指回0,mark置为-1。而clear操作则将limit重置为capacity,position重置为0,mark置为-1。
这里的clear是一种逻辑上的清除操作,内容并没有真的消除。例如上面的读例子中,我们将 byteBufferHeap.flip();替换为 byteBufferHeap.clear();运行结果如下:
在这里插入图片描述
我们可以看到,内容仍然可以读取出来。使用clear的目的是方便我们重新读取数据到ByteBuffer中。当我们理解了flip()和clear()的意思之后,我们才知道什么时候该用哪个方法:
当我们第一次读取Channel内容到ByteBuffer中的时候,这时position已经是读取的长度,为了获取ByteBuffer的内容,我们需要flip()一下,使得position重新回到0,然后就可以读取ByteBuffer中的内容了。当我们需要向ByteBuffer重新写内容的时候(覆盖),我们需要clear()一下,然后读取Channel内容到ByteBuffer中。
当我们需要将ByteBuffer的内容写回Channel的时候,我们需要flip()一下,然后写入Channel。写Channel的时候,clear()没什么使用场景。
为什么需要flip()这么奇怪的方法呢?从Channel读取内容后,直接读取ByteBuffer多好!据Thinking in java说,这么做是为了效率!但是似乎没看到相关的解释,猜测和position的使用有关。

mark()和reset()

mark()可以立即保存当前的position(),方便后面使用reset()复位到之前的position的位置,所以mark()和reset()这里放在了一起进行说明。在RocketMQ中,mark()的一个使用场景是:
写入message前先记录ByteBuffer的position位置,然后保存内容到文件。如果下次读取的内容,文件已经没有剩余的空间保存了,那么需要使用reset()恢复到上次读取的位置。

rewind()

rewind类似于flip(),但是它只重置position=0,mark=-1,对于limit它是不处理的,默认limit已经在正确的位置。

堆内内存和堆外内存

之前的例子,创建的ByteBuffer都是分配的堆内内存,也就是jvm里面的内存。下面的例子,我们通过allocateDirect()申请堆外内存:

 public static void main(String[] args) throws Exception{ 
   
        String path = "d:" + File.separator + "bill";
        FileChannel fileChannel = new FileInputStream(path).getChannel();

        final int CAPACITY = 20;
        ByteBuffer byteBufferHeap = ByteBuffer.allocateDirect(CAPACITY); // 申请堆外内存

        while(fileChannel.read(byteBufferHeap) != -1){ 
   // 读取内容到byteBuffer中,直到文件末尾
            byteBufferHeap.flip();// 翻转byteBuffer,为了后面读取byteBuffer内容

            while(byteBufferHeap.hasRemaining()){ 
    // ByteBuffer还有内容
                System.out.print((char)byteBufferHeap.get());
            }

            byteBufferHeap.clear();// 清空byteBuffer,为了后面读取内容到ByteBuffer。
        }
    }

allocateDirect返回的是DirectByteBuffer对象,其继承关系如下所示(这个图是后面参考文章的):
在这里插入图片描述DirectByteBuffer的使用与ByteBuffer几乎没什么区别,但是它们所蕴含的意思大不一样。DirectByteBuufer使用的是堆外内存,而我们平时的ByteBuffer使用的是堆内内存。我们之前介绍属性的时候,有下面两个属性:

   long address; // 访问地址,仅仅针对DirectByteBuffer,使用的是堆外内存。
   final byte[] hb; // 堆内内存时存放的内容

分配堆外内存,会返回堆外内存的地址,并存放在address里面。如果是堆内内存,那么内容就存放在hb里面。所以这两者是互斥的。
对于需要频繁操作的内存,并且仅仅是临时存在一会的,都建议使用堆外内存,并且做成缓冲池,不断循环利用这块内存。
更详情的内容可以查看后面的参考文章,这里限于篇幅和知识,就不画蛇添足了。

零拷贝技术

RocketMQ号称使用了零拷贝技术,其实就是这里的堆外内存。应用程序读取磁盘文件,看似很简单,其实在操作系统底层做了多次拷贝。首先系统将磁盘文件拷贝进入内核缓冲区,然后再切换到用户态,将内容拷贝到用户的内存缓冲区。这里不仅有系统内核态、用户态的切换,而且还有多次拷贝。如果使用了堆外内存,那么系统内核将与应用共享一片缓存区,那么磁盘文件从磁盘拷贝到共享缓冲区后,应用程序就可以直接访问了,从而免去了拷贝操作。这里讲的很粗糙,如果感兴趣可以查看后面的参考文章。

好了,到这里我们基本上对ByteBuffer有了一个大致的了解,后面就是RockeMQ消息三部曲:消息发送、消息存储和消息消费的内容了,敬请期待。

参考文章

1-浅析Linux中的零拷贝技术
2-JVM源码分析之堆外内存完全解读
3-堆外内存之DirectByteBuffer
4-JAVA NIO之浅谈内存映射文件原理与DirectMemory
5-堆内内存还是堆外内存?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/186597.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 剑指 Offer 06. 从尾到头打印链表(链表)

    剑指 Offer 06. 从尾到头打印链表(链表)输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。示例 1:输入:head = [1,3,2]输出:[2,3,1]限制:0 <= 链表长度 <= 10000题解链表/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} *

  • ps磨皮滤镜插件Portraiture 3

    ps磨皮滤镜插件Portraiture 3Photoshop必备滤镜插件,Portraiture3formac(ps磨皮滤镜插件)是一款支持自动皮肤平滑、愈合和增强效果的磨皮插件,portraiture3主要针对人像进行皮肤修饰、磨皮润色等处理,还可以平滑和去除缺陷,同时保留皮肤纹理和重要的人像细节,功能十分强大,安装即为激活成功教程版。ps磨皮滤镜插件特色介绍Photoshop的画像消除了选择性蒙版和逐像素处理的繁琐的手工劳动,以帮助您在肖像修饰中实现卓越。Imagenomic发布了Photoshop的磨皮滤镜Portraiture

  • Objective-C路成魔【18-复制对象】[通俗易懂]

    Objective-C路成魔【18-复制对象】

  • git 迁出/克隆远程仓库的指定分支方法(附常用git配置命令)

    普通克隆方式:gitclone&lt;远程仓库地址&gt;这种克隆方式默认是克隆master主分支,而且通过命令gitbranch–list能看到克隆后在本地也只有这一个分支,如果再通过新建分支再拉取指定分支,甚至可能还需要解决冲突,太繁琐。那么,如何快速有效的直接克隆远程指定分支?只需要一条命令:gitclone-b&lt;指定分支名&gt;&…

  • navicate 15 激活码(JetBrains全家桶)

    (navicate 15 激活码)最近有小伙伴私信我,问我这边有没有免费的intellijIdea的激活码,然后我将全栈君台教程分享给他了。激活成功之后他一直表示感谢,哈哈~https://javaforall.cn/100143.htmlIntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,上面是详细链接哦~MLZP…

  • booth算法原理的简单化理解「建议收藏」

    booth算法原理的简单化理解「建议收藏」最近,在学习带符号二进制数乘法(multiplicationof signednumbers)时接触到了布思算法(boothalgorithm)。由于是第一次接触,对于其原理却一无所知,书上的解释以及网上的文章不知是自己才疏学浅还本来就是泛泛而谈,没有让我了解其本质。经过长时间的思考分析,最终找到了一种比较简单的理解方法。举一个简单的例子,比如说计算10100001×00111110,

    2022年10月23日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号