zoukankan      html  css  js  c++  java
  • Buffer的创建及使用源码分析——ByteBuffer为例

    目录

    • Buffer概述
    • Buffer的创建
    • Buffer的使用
    • 总结
    • 参考资料

    Buffer概述

    注:全文以ByteBuffer类为例说明
    Java中提供了7种类型的Buffer,每一种类型的Buffer根据分配内存的方式不同又可以分为
    直接缓冲区和非直接缓冲区。

    Buffer的本质是一个定长数组,并且在创建的时候需要指明Buffer的容量(数组的长度)。
    而这个数组定义在不同的Buffer当中。例如ByteBuffer的定义如下:

    public abstract class ByteBuffer
        extends Buffer
        implements Comparable<ByteBuffer>
    {
    
        // These fields are declared here rather than in Heap-X-Buffer in order to
        // reduce the number of virtual method invocations needed to access these
        // values, which is especially costly when coding small buffers.
        //
        //在这里定义Buffer对应的数组,而不是在Heap-X-Buffer中定义
        //目的是为了减少访问这些纸所需的虚方法调用,但是对于小的缓冲区,代价比较高
        final byte[] hb;                  // Non-null only for heap buffers
        final int offset;
        boolean isReadOnly;                 // Valid only for heap buffers
    
        // Creates a new buffer with the given mark, position, limit, capacity,
        // backing array, and array offset
        //
        ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                     byte[] hb, int offset)
        {
            //调用父类Buffer类的构造函数构造
            super(mark, pos, lim, cap);
            this.hb = hb;
            this.offset = offset;
        }
    
        // Creates a new buffer with the given mark, position, limit, and capacity
        //
        ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
            this(mark, pos, lim, cap, null, 0);
        }
    ......
    }
    

    尽管数组在这里定义,但是这个数组只对非直接缓冲区有效。

    ByteBuffer类有两个子类分别是:DirectByteBuffer(直接缓冲区类)和HeapByteBuffer(非直接缓冲区)。
    但是这两个类并不能直接被访问,因为这两个类是包私有的,而创建这两种缓冲区的方式就是通过调用Buffer
    类提供的创建缓冲区的静态方法:allocate()allocateDirect()

    Buffer的创建

    Buffer要么是直接的要么是非直接的,非直接缓冲区的内存分配在JVM内存当中,
    而直接缓冲区使用物理内存映射,直接在物理内存中分配缓冲区,既然分配内存的地方不一样,
    BUffer的创建方式也就不一样。

    非直接缓冲区内存的分配

    创建非直接缓冲区可以通过调用allocate()方法,这样会将缓冲区建立在JVM内存(堆内存)当中。
    allocate()方法是一个静态方法,因此可以直接使用类来调用。
    具体的创建过程如下:

        /**
         * Allocates a new byte buffer.
         *
         * <p> The new buffer's position will be zero, its limit will be its
         * capacity, its mark will be undefined, and each of its elements will be
         * initialized to zero.  It will have a {@link #array backing array},
         * and its {@link #arrayOffset array offset} will be zero.
         *
         * @param  capacity
         *         The new buffer's capacity, in bytes
         *
         * @return  The new byte buffer
         *
         * @throws  IllegalArgumentException
         *          If the <tt>capacity</tt> is a negative integer
         */
        //分配一个缓冲区,最后返回的其实是一个HeapByteBuffer的对象
        public static ByteBuffer allocate(int capacity) {
            if (capacity < 0)
                throw new IllegalArgumentException();
            //这里调用到HeapByteBuffer类的构造函数,创建非直接缓冲区
            //并将需要的Buffer容量传递
            //从名称也可以看出,创建的位置在堆内存上。
            return new HeapByteBuffer(capacity, capacity);
        }
    

    HeapByteBuffer(capacity, capacity)用于在堆内存上创建一个缓冲区。
    该方法优惠调回ByteBuffer构造方法,HeapByteBuffer类没有任何的字段,他所需的字段全部定义在父类当中。
    源码分析如下:

        HeapByteBuffer(int cap, int lim) {
            // 调用父类的构造方法创建非直接缓冲区           // package-private
            // 调用时根据传递的容量创建了一个数组。
            super(-1, 0, lim, cap, new byte[cap], 0);
        }
        
        //ByteBuffer类的构造方法,也就是上面代码调用的super方法
        ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                         byte[] hb, int offset)
            {
                //接着调用Buffer类的构造方法给用于操作数组的四个属性赋值
                super(mark, pos, lim, cap);
                //将数组赋值给ByteBuffer的hb属性,
                this.hb = hb;
                this.offset = offset;
            }
    
        //Buffer类的构造方法
        Buffer(int mark, int pos, int lim, int cap) {       // package-private
            //容量参数校验,原始容量不能小于0
            if (cap < 0)
                throw new IllegalArgumentException("Negative capacity: " + cap);
            //设定容量
            this.capacity = cap;
            //这里的lim从上面传递过来的时候就是数组的容量
            //limit在写模式下默认可操作的范围就是整个数组
            //limit在读模式下可以操作的范围是数组中写入的元素
            //创建的时候就是写模式,是整个数组
            limit(lim);
            //初始的position是0
            position(pos);
            //设定mark的值,初始情况下是-1,因此有一个参数校验,
            //-1是数组之外的下标,不可以使用reset方法使得postion到mark的位置。
            if (mark >= 0) {
                if (mark > pos)
                    throw new IllegalArgumentException("mark > position: ("
                                                       + mark + " > " + pos + ")");
                this.mark = mark;
            }
        }
    

    在堆上创建缓冲区还是很简单的,本质就是创建了一个数组以及一些用于辅助操作数组的其他属性。

    最后返回的其实是一个HeapByteBuffer的对象,因此对其的后续操作大多应该是要调用到HeapByteBuffer类中

    直接缓冲区的创建

    创建直接俄缓冲区可以通过调用allocateDirect()方法创建,源码如下:

        /**
         * Allocates a new direct byte buffer.
         *
         * <p> The new buffer's position will be zero, its limit will be its
         * capacity, its mark will be undefined, and each of its elements will be
         * initialized to zero.  Whether or not it has a
         * {@link #hasArray backing array} is unspecified.
         *
         * @param  capacity
         *         The new buffer's capacity, in bytes
         *
         * @return  The new byte buffer
         *
         * @throws  IllegalArgumentException
         *          If the <tt>capacity</tt> is a negative integer
         */
        //创建一个直接缓冲区
        public static ByteBuffer allocateDirect(int capacity) {
            //同非直接缓冲区,都是创建的子类的对象
            //创建一个直接缓冲区对象
            return new DirectByteBuffer(capacity);
        }
    

    DirectByteBuffer(capacity)DirectByteBuffer的构造函数,具体代码如下:

        DirectByteBuffer(int cap) {                   // package-private
            //初始化mark,position,limit,capacity
            super(-1, 0, cap, cap);
            //内存是否按页分配对齐,是的话,则实际申请的内存可能会增加达到对齐效果
            //默认关闭,可以通过-XX:+PageAlignDirectMemory控制
            boolean pa = VM.isDirectMemoryPageAligned();
            //获取每页内存的大小
            int ps = Bits.pageSize();
            //分配内存的大小,如果是按页对其的方式,需要加一页内存的容量
            long size = Math.max(1L, (long)cap + (pa ? ps : 0));
            //预定内存,预定不到则进行回收堆外内存,再预定不到则进行Full gc
            Bits.reserveMemory(size, cap);
            
            long base = 0;
            try {
                //分配堆外内存
                base = unsafe.allocateMemory(size);
            } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
            }
            unsafe.setMemory(base, size, (byte) 0);
            if (pa && (base % ps != 0)) {
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));
            } else {
                address = base;
            }
            /**
             *创建堆外内存回收Cleanner,Cleanner对象是一个PhantomFerence幽灵引用,
             *DirectByteBuffer对象的堆内存回收了之后,幽灵引用Cleanner会通知Reference
             *对象的守护进程ReferenceHandler对其堆外内存进行回收,调用Cleanner的
             *clean方法,clean方法调用的是Deallocator对象的run方法,run方法调用的是
             *unsafe.freeMemory回收堆外内存。
            
             *堆外内存minor gc和full gc的时候都不会进行回收,而是ReferenceHandle守护进程调用
             *cleanner对象的clean方法进行回收。只不过gc 回收了DirectByteBuffer之后,gc会通知Cleanner进行回收
             */
            cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
            att = null;
    
        }
    

    由于是在物理内存中直接分配一块内存,而java并不直接操作内存需要交给JDKnative方法的实现分配

    Bits.reserveMemory(size, cap)预定内存源码,预定内存,说穿了就是检查堆外内存是否足够分配

        // These methods should be called whenever direct memory is allocated or
        // freed.  They allow the user to control the amount of direct memory
        // which a process may access.  All sizes are specified in bytes.
        // 在分配或释放直接内存时应当调用这些方法,
        // 他们允许用控制进程可以访问的直接内存的数量,所有大小都以字节为单位
        static void reserveMemory(long size, int cap) {
            //memoryLimitSet的初始值为false
            //获取允许的最大堆外内存赋值给maxMemory,默认为64MB
            //可以通过-XX:MaxDirectMemorySize参数控制
            if (!memoryLimitSet && VM.isBooted()) {
                maxMemory = VM.maxDirectMemory();
                memoryLimitSet = true;
            }
    
            // optimist!
            //理想情况,maxMemory足够分配(有足够内存供预定)
            if (tryReserveMemory(size, cap)) {
                return;
            }
    
            final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
    
            // retry while helping enqueue pending Reference objects
            // which includes executing pending Cleaner(s) which includes
            // Cleaner(s) that free direct buffer memory
            // 这里会尝试回收堆外空间,每次回收成功尝试进行堆外空间的引用
            while (jlra.tryHandlePendingReference()) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
            }
    
            // trigger VM's Reference processing
            // 依然分配失败尝试回收堆空间,触发full gc
            // 
            System.gc();
    
            // a retry loop with exponential back-off delays
            // (this gives VM some time to do it's job)
            boolean interrupted = false;
            
            // 接下来会尝试最多9次的内存预定,应该说是9次的回收堆外内存失败的内存预定
            // 如果堆外内存回收成功,则直接尝试一次内存预定,只有回收失败才会sleep线程。
            // 每次预定的时间间隔为1ms,2ms,4ms,等2的幂递增,最多256ms。
            try {
                long sleepTime = 1;
                int sleeps = 0;
                while (true) {
                    // 尝试预定内存
                    if (tryReserveMemory(size, cap)) {
                        return;
                    }
                    if (sleeps >= MAX_SLEEPS) {
                        break;
                    }
                    // 预定内存失败则进行尝试释放堆外内存,
                    // 累计最高可以允许释放堆外内存9次,同时sleep线程,对应时间以2的指数幂递增
                    if (!jlra.tryHandlePendingReference()) {
                        try {
                            Thread.sleep(sleepTime);
                            sleepTime <<= 1;
                            sleeps++;
                        } catch (InterruptedException e) {
                            interrupted = true;
                        }
                    }
                }
    
                // no luck
                throw new OutOfMemoryError("Direct buffer memory");
    
            } finally {
                if (interrupted) {
                    // don't swallow interrupts
                    Thread.currentThread().interrupt();
                }
            }
        }
    

    为什么调用System.gc?引用自JVM原始码分析之堆外内存完全解读

    既然要调用System.gc,那肯定是想通过触发一次gc操作来回收堆外部内存,不过我想先说的是堆外部内存不会对gc造成什么影响(这里的System.gc除外),但是堆外层内存的回收实际上依赖于我们的gc机制,首先我们要知道在java尺寸和我们在堆外分配的这块内存分配的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过DirectByteBuffer对象来间接操作对应的堆外部内存了。DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference时被回收的,它不能影响gc方法,但是gc过程中如果发现某个对象只有只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref .Reference.pending物理里,在gc完成的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReferencePhantomReference的一个子类,在最终的处理里会通过Unsafe的免费接口来释放DirectByteBuffer对应的堆外内存块

    Buffer的使用

    切换读模式flip()

    切换为读模式的代码分厂简单,就是使limit指针指向buffer中最后一个插入的元素的位置,即position,指针的位置。
    position代表操作的位置,那么从0开始,所以需要将position指针归0.源码如下:

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

    get()读取

    get()读取的核心是缓冲区对应的数组中取出元素放在目标数组中(get(byte[] dst)方法是有一个参数的,传入的就是目标数组)。

        public ByteBuffer get(byte[] dst) {
            return get(dst, 0, dst.length);
        }
    
        public ByteBuffer get(byte[] dst, int offset, int length) {
            checkBounds(offset, length, dst.length);
            if (length > remaining())
                throw new BufferUnderflowException();
            int end = offset + length;
            //shiyongfor循环依次放入目标数组中
            for (int i = offset; i < end; i++)
                // get()对于直接缓冲区和非直接缓冲区是不一样的,所以交由子类实现。
                dst[i] = get();
            return this;
        }
    

    rewind()重复读

    既然要重复读就需要把position置0了

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

    clear()清空缓冲区与compact()方法

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

    clear()方法中,仅仅是将三个指针还原为创建时的状态供后续写入,但是之前写入的数据并没有被删除,依然可以使用get(int index)获取

    但是有一种情况,缓冲区已经满了还想接着写入,但是没有读取完又不能从头开始写入该怎么办,答案是compact()方法

    非直接缓冲区:
       public ByteBuffer compact() {
            //将未读取的部分拷贝到缓冲区的最前方
            System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
            //设置position位置到缓冲区下一个可以写入的位置
            position(remaining());
            //设置limit是最大容量
            limit(capacity());
            //设置mark=-1
            discardMark();
            return this;
        }
    
    直接缓冲区:
        public ByteBuffer compact() {    
            int pos = position();
            int lim = limit();
            assert (pos <= lim);
            int rem = (pos <= lim ? lim - pos : 0);
            //调用native方法拷贝未读物部分
            unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
            //设定指针位置
            position(rem);
            limit(capacity());
            discardMark();
            return this;
        }
    

    mark()标记位置以及reset()还原

    mark()标记一个位置,准确的说是当前的position位置

        public final Buffer mark() {
            mark = position;
            return this;
        }
    

    标记了之后并不影响写入或者读取,position指针从这个位置离开再次想从这个位置读取或者写入时,
    可以使用reset()方法

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

    总结

    本文其实还有很多不清楚的地方,对于虚引用以及引用队列的操作还不是很清楚去,对于虚引用和堆外内存的回收的关系源码其实也没看到,
    需要再看吧,写这篇的目的其实最开始就是想研究看看直接缓冲区内存的分配,没想到依然糊涂,后面填坑。路过的大佬也就指导下虚引用这部分相关的东西,谢谢。

    参考资料

  • 相关阅读:
    动态开点线段树
    Codeforces Round #524 (Div. 2) F. Katya and Segments Sets(主席树)
    Codeforces Round #524 (Div. 2) E. Sonya and Matrix Beauty(字符串哈希,马拉车)
    Codeforces Round #523 (Div. 2) E. Politics(最小费+思维建图)
    UVA12118 Inspector's Dilemma(欧拉路径)
    UVA
    UVA-127 "Accordian" Patience(模拟)
    UVA-1599 Ideal Path(双向BFS)
    UVA-1572 Self-Assembly(拓扑排序判断有向环)
    UVA-122 Trees on the level(树的遍历)
  • 原文地址:https://www.cnblogs.com/bmilk/p/13255546.html
Copyright © 2011-2022 走看看