NIO 之 ByteBuffer实现原理

作者: jijs | 来源:发表于2017-08-17 00:04 被阅读733次

    相关文章

    IO、NIO、AIO 内部原理分析
    NIO 之 Selector实现原理
    NIO 之 Channel实现原理

    前言

    Java NIO 主要由下面3部分组成:

    • Buffer
    • Channel
    • Selector

    在传统IO中,流是基于字节的方式进行读写的。
    在NIO中,使用通道(Channel)基于缓冲区数据块的读写。

    流是基于字节一个一个的读取和写入。
    通道是基于块的方式进行读取和写入。

    Buffer 类结构图

    Buffer 的类结构图如下:


    Buffer类结构图

    从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。

    Buffer类分析

    Filed

    每个缓冲区都有这4个属性,无论缓冲区是何种类型都有相同的方法来设置这些值

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    

    1. 标记(mark)

    初始值-1,表示未标记。
    标记一个位置,方便以后reset重新从该位置读取数据。

    public final Buffer mark() {
        mark = position;
        return this;
    }
    
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
    

    2. 位置(position)

    缓冲区中读取或写入的下一个位置。这个位置从0开始,最大值等于缓冲区的大小

    //获取缓冲区的位置
    public final int position() {
        return position;
    }
    //设置缓冲区的位置
    public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }
    

    3. 限度(limit)

    //获取limit位置
    public final int limit() {
        return limit;
    }
    //设置limit位置
    public final Buffer limit(int newLimit) {
        if ((newLimit > capacity) || (newLimit < 0))
            throw new IllegalArgumentException();
        limit = newLimit;
        if (position > limit) position = limit;
        if (mark > limit) mark = -1;
        return this;
     }
    

    4. 容量(capacity)

    缓冲区可以保存元素的最大数量。该值在创建缓存区时指定,一旦创建完成后就不能修改该值。

    //获取缓冲区的容量
    public final int capacity() {
        return capacity;
    }
    

    filp 方法

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    
    1. 将limit设置成当前position的坐标
    2. 将position设置为0
    3. 取消标记

    rewind 方法

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    

    从源码中发现,rewind修改了position和mark,而没有修改limit。

    1. 将position设置为0
    2. 取消mark标记

    clear 方法

        public final Buffer clear() {
            position = 0;
            limit = capacity;
            mark = -1;
            return this;
        }
    
    1. 将position坐标设置为0
    2. limit设置为capacity
    3. 取消标记

    从clear方法中,我们发现Buffer中的数据没有清空,如果通过Buffer.get(i)的方式还是可以访问到数据的。如果再次向缓冲区中写入数据,他会覆盖之前存在的数据。

    remaining 方法

    查看当前位置和limit之间的元素数。

    public final int remaining() {
        return limit - position;
    }
    

    hasRemaining 方法

    判断当前位置和limit之间是否还有元素

    public final boolean hasRemaining() {
        return position < limit;
    }
    

    ByteBuffer 类分析

    ByteBuffer类结果图

    从图中我们可以发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类

    HeapByteBuffer:在堆中创建的缓冲区。就是在jvm中创建的缓冲区。
    MappedByteBuffer:直接缓冲区。物理内存中创建缓冲区,而不在堆中创建。

    allocate 方法(创建堆缓冲区)

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }
    

    我们发现allocate方法创建的缓冲区是创建的HeapByteBuffer实例。

    HeapByteBuffer 构造

    HeapByteBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new byte[cap], 0);
    }
    

    从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中创建一个byte[]数组。

    allocateDirect创建直接缓冲区

    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    

    我们发现allocate方法创建的缓冲区是创建的DirectByteBuffer实例。

    DirectByteBuffer构造

    DirectByteBuffer 构造方法

    直接缓冲区是通过java中Unsafe类进行在物理内存中创建缓冲区。

    wrap 方法

    public static ByteBuffer wrap(byte[] array)
    public static ByteBuffer wrap(byte[] array, int offset, int length);
    

    可以通过wrap类把字节数组包装成缓冲区ByteBuffer实例。
    这里需要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。如果array数组中的值更改,则ByteBuffer中的数据也会更改的。

    get 方法

    1. public byte get()
      获取position坐标元素,并将position+1;
    2. public byte get(int i)
      获取指定索引下标的元素
    3. public ByteBuffer get(byte[] dst)
      从当前position中读取元素填充到dst数组中,每填充一个元素position+1;
    4. public ByteBuffer get(byte[] dst, int offset, int length)
      从当前position中读取元素到dst数组的offset下标开始填充length个元素。

    put 方法

    1. public ByteBuffer put(byte x)
      写入一个元素并position+1
    2. public ByteBuffer put(int i, byte x)
      指定的索引写入一个元素
    3. public final ByteBuffer put(byte[] src)
      写入一个自己数组,并position+数组长度
    4. public ByteBuffer put(byte[] src, int offset, int length)
      从一个自己数组的offset开始length个元素写入到ByteBuffer中,并把position+length
    5. public ByteBuffer put(ByteBuffer src)
      写入一个ByteBuffer,并position加入写入的元素个数

    视图缓冲区

    Paste_Image.png

    ByteBuffer可以转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。

    压缩缓冲区

    public ByteBuffer compact() {
            System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
            position(remaining());
            limit(capacity());
            discardMark();
            return this;
        }
    

    1、把缓冲区positoin到limit中的元素向前移动positoin位
    2、设置position为remaining()
    3、 limit为缓冲区容量
    4、取消标记

    例如:ByteBuffer.allowcate(10);
    内容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]

    compact前

    [0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
    pos=4
    lim=10
    cap=10

    compact后

    [4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
    pos=6
    lim=10
    cap=10

    slice方法

    public ByteBuffer slice() {
            return new HeapByteBuffer(hb,
                        -1,
                        0,
                        this.remaining(),
                        this.remaining(),
                        this.position() + offset);
    }
    

    创建一个分片缓冲区。分配缓冲区与主缓冲区共享数据。
    分配的起始位置是主缓冲区的position位置
    容量为limit-position。
    分片缓冲区无法看到主缓冲区positoin之前的元素。

    直接缓冲区和堆缓冲区性能对比

    下面我们从缓冲区创建的性能和读取性能两个方面进行性能对比。

    读写性能对比

    public static void directReadWrite() throws Exception {
        int time = 10000000;
        long start = System.currentTimeMillis();
        ByteBuffer buffer = ByteBuffer.allocate(4*time);
        for(int i=0;i<time;i++){
            buffer.putInt(i);
        }
        buffer.flip();
        for(int i=0;i<time;i++){
            buffer.getInt();
        }
        System.out.println("堆缓冲区读写耗时  :"+(System.currentTimeMillis()-start));
        
        start = System.currentTimeMillis();
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
        for(int i=0;i<time;i++){
            buffer2.putInt(i);
        }
        buffer2.flip();
        for(int i=0;i<time;i++){
            buffer2.getInt();
        }
        System.out.println("直接缓冲区读写耗时:"+(System.currentTimeMillis()-start));
    }
    

    输出结果:

    堆缓冲区创建耗时  :70
    直接缓冲区创建耗时:47
    

    从结果中我们发现堆缓冲区读写比直接缓冲区读写耗时更长。

    public static void directAllocate() throws Exception {
        int time = 10000000;
        long start = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            ByteBuffer buffer = ByteBuffer.allocate(4);
        }
        System.out.println("堆缓冲区创建时间:"+(System.currentTimeMillis()-start));
            
        start = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(4);
        }
        System.out.println("直接缓冲区创建时间:"+(System.currentTimeMillis()-start));
    }
    

    输出结果:

    堆缓冲区创建时间:73
    直接缓冲区创建时间:5146
    

    从结果中发现直接缓冲区创建分配空间比较耗时。

    对比结论

    直接缓冲区比较适合读写操作,最好能重复使用直接缓冲区并多次读写的操作。
    堆缓冲区比较适合创建新的缓冲区,并且重复读写不会太多的应用。

    建议:如果经过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,否则不建议使用直接缓冲区。


    想了解更多精彩内容请关注我的公众号

    相关文章

      网友评论

      • 私奔_1f4f:position我理解为当前可读写的位置
        jijs:具体参考这篇文章:https://www.jianshu.com/p/12c81abb5387
        私奔_1f4f:我是以下标0开始的,position=3., 意味着我下一次操作的元素下标为3。
        jijs:比如:buffer 长度为10
        写了三个元素 A、 B、 C。这时 position的值为3,指向第四个坐标

      本文标题:NIO 之 ByteBuffer实现原理

      本文链接:https://www.haomeiwen.com/subject/tywwrxtx.html