美文网首页程序员
Java nio — ByteBuffer

Java nio — ByteBuffer

作者: RoyRuan | 来源:发表于2018-01-20 16:18 被阅读0次

    缓冲区本质是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包裹成NIO buffer对象,并且提供了一些方法来访问该块内存。

    1.ByteBuffer的分配

    ByteBuffer是一个抽象类,所以我们不能直接通过new 来创建我们需要的缓冲对象,当然也不用通过重写抽象方法来创建对象。

    ByteBuffer内部提供了两个静态方法来帮助我们创建内存:

    分别是分配HeapByteBuffer的:

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

    分配DirectByteBuffer的:

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

    都是分配内存,为什么需要两种不同的方法呢?其实我们从他们的名字就能看出来,前者分配内存的区域是在Heap区域,当flush到远程的时候会拷贝到直接内存。而后者则是在C heap上分配内存,可以得到非常快速的网络交互。

    HeapByteBuffer和DirectByteBuffer都是包内私有,无法被我们访问的。只能通过ByteBuffer间接访问。

    通过源码,我们能看到创建实例的方法如下:

    HeapByteBuffer:

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

    该缓冲区依旧委托给父类进行创建。除了调用父类构造方法,也没有其他的操作。
    我们来看一看创建一块ByteBuffer需要哪些参数:

    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                     byte[] hb, int offset)
        {
            super(mark, pos, lim, cap);
            this.hb = hb;
            this.offset = offset;
        }
    

    还记得吗?我们在调用allocate()方法的时候仅仅向里面传递了一个cap。
    而HeapByteBuffer传回将lim = cap, pos = 0, offset = 0 mark = -1 (即 undefine)
    这几个参数一会儿我们来讨论其意义。

    继续向上调用构造函数

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
            if (cap < 0)
                throw new IllegalArgumentException("Negative capacity: " + cap);
            this.capacity = cap;
            limit(lim);
            position(pos);
            if (mark >= 0) {
                if (mark > pos)
                    throw new IllegalArgumentException("mark > position: ("
                                                       + mark + " > " + pos + ")");
                this.mark = mark;
            }
        }
    

    调用的是Buffer的构造函数,其实现类很多,在此不论。

    创建完成之后,此时内存空间重要参数布局是这样的:


    bt1.png

    DirectByteBuffer

    DirectByteBuffer(int cap) {                   // package-private
            super(-1, 0, cap, cap);
            boolean pa = VM.isDirectMemoryPageAligned();
            int ps = Bits.pageSize();
            long size = Math.max(1L, (long)cap + (pa ? ps : 0));
            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;
            }
            cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
            att = null;
        }
    

    由于该缓冲区是堆外申请的内存,我们可以看到除了调用父类的构造函数,还进行了很多其他操作。
    该类继承自MappedByteBuffer类,关于该类的具体资料:
    http://blog.csdn.net/linxdcn/article/details/72903422

    这些额外的操作主要就要获取address(堆外内存地址),还有就是直接内存不是分配在JVM堆中的,而是用JNI直接调用的内存。并不受Minor GC的影响,只有当老年代执行Full gc的时候才会回收直接内存。而只有当众多DirectByteBuffer被送入老年代后才会触发Full gc。

    关于堆外内存的回收机制:
    http://www.importnew.com/26334.html

    分配后内存参数和HeapByteBuffer相同。

    这两种内存空间模型如下:

    HeapByteBuffer

    JVM Heap <----> JVM用户空间 <----> OS内核空间<----->网卡驱动空间;

    DirectByteBuffer

    JVM用户空间 <----> OS内核空间<----->网卡驱动空间。

    directBytebuffer虽然看似要快许多,但在数据量较少的时候并无太大的优势,而且内存需要手动释放,容易出现问题。
    所以如果没有性能瓶颈尽可能使用HeapByteBuffer来作为缓冲区。

    2.ByteBuffer的读写及参数意义:

    主要参数有:

    • capacity
    • position
    • limit
    • mark
      关于bytebuffer的读写原理见插图:


      image

    capacity

    作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

    position

    当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

    当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

    limit

    在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

    当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

    除了以上方法外,还能使用类似bytebuffer.put(127)向缓冲区中写数据。

    了解了这几个参数的意义后,我们来看看几个重要方法的

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

    flip()方法应该是最常用的方法,因为bytebuffer的结构,在读数据的时候,我们会将pos = 0, limit = pos,这个意思就是我们可以并且只能读取我们之前写入的数据。

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

    如源码所示,将一切属性设置为来初始化状态。

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

    将pos=0,可以重新读取所有内容。

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

    读写模式下表示可读/可写范围。

    compact()

    方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
    由于此处涉及到分配内存,所以HeapByteBuffer和DirectByteBuffer的实现又不一样。详情见源码。

    在前面我们已经讨论过了pos, lim, cap三个参数,还有一个mark参数未讨论。
    看其字面意思,其实就是标记的意思。

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

    将mark=pos.
    该参数需要配合

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

    当我们需要记住前一个pos的时候(此时,我们已经调用了rewind或者clear了),可以使用mark参数。

    equals()与compareTo()方法

    equals()

    当满足下列条件时,表示两个Buffer相等:

    有相同的类型(byte、char、int等)。
    Buffer中剩余的byte、char等的个数相等。
    Buffer中所有剩余的byte、char等都相同。
    如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

    compareTo()方法

    compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:

    第一个不相等的元素小于另一个Buffer中对应的元素 。
    所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

    剩余元素 : pos -> limit

    参考资料:

    相关文章

      网友评论

        本文标题:Java nio — ByteBuffer

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