图解ByteBuffer

图片 12

本篇主要讲解如何使用直接内存(堆外内存),并按照下面的步骤进行说明:

ByteBuffer前前后后看过好几次了,实际使用也用了一些,总觉得条理不够清晰。

Buffer是java
nio中的一个抽象类,表示数据的内存缓冲区。一个buffer是一个线性的、有限的特定原生类型的数据容器,可对其进行读写操作,且一般会与nio另外两个重要的组件Channel、selector搭配使用。

相关背景-->读写操作-->关键属性-->读写实践-->扩展-->参考说明

《程序员的思维修炼》一本书讲过,主动学习,要比单纯看资料效果来的好,所以干脆写个详细点的文章来记录一下。

java的原生数据类型中除了boolean外,其余的都有与之对应的特化版本的Buffer,比如ByteBuffer、CharBuffer、IntBuffer等。在日常开发中,一般会使用特化的Buffer,比如最常用的ByteBuffer。

希望对想使用直接内存的朋友,提供点快捷的参考。

概述

ByteBuffer是NIO里用得最多的Buffer,它包含两个实现方式:HeapByteBuffer是基于Java堆的实现,而DirectByteBuffer则使用了unsafe的API进行了堆外的实现。这里只说HeapByteBuffer。

三大属性

若要掌握Buffer的原理与使用方式,必须先了解其三个重要的属性:capacity、limit、position。

capacity:buffer容器的大小。表明该容器最多能容纳多少个元素,且该属性值是不能改变。

limit:限制对buffer的读写操作。标明该limit索引上的位置无法进行读取或者写入操作,该属性值不能大于capacity,该值会随着flip和clear方法的调用而发生改变。

position:下一次可对buffer的读取或者写入操作的位置索引,不能大于limit,该值会随着读写操作而发生变化。

下面以ByteBuffer为例,简单介绍下其原理与常用的方法。

数据类型

下面这些,都是在使用DirectBuffer中必备的一些常识,暂作了解吧!如果想要深入理解,可以看看下面参考的那些博客。

使用

ByteBuffer最核心的方法是put(byte)get()。分别是往ByteBuffer里写一个字节,和读一个字节。

值得注意的是,ByteBuffer的读写模式是分开的,正常的应用场景是:往ByteBuffer里写一些数据,然后flip(),然后再读出来。

这里插两个Channel方面的对象,以便更好的理解Buffer。

ReadableByteChannel是一个从Channel中读取数据,并保存到ByteBuffer的接口,它包含一个方法:

public int``read(ByteBuffer dst) ``throws``IOException;

 

WritableByteChannel则是从ByteBuffer中读取数据,并输出到Channel的接口:

public int``write(ByteBuffer src) ``throws``IOException;

 

那么,一个ByteBuffer的使用过程是这样的:

  1. byteBuffer = ByteBuffer.allocate(N);    //创建

  2. readableByteChannel.read(byteBuffer);   //读取数据,写入byteBuffer

  3. byteBuffer.flip();              //变读为写

4. writableByteChannel.write(byteBuffer);   //读取byteBuffer,写入数据

 

看到这里,一般都不太明白flip()干了什么事,先从ByteBuffer结构说起:

 

ByteBuffer的创建和读写

 

  1. ByteBuffer定义了4个static方法来做创建工作:

  ByteBuffer allocate(int capacity)
//创建一个指定capacity的ByteBuffer。
  ByteBuffer allocateDirect(int capacity)
//创建一个direct的ByteBuffer,这样的ByteBuffer在参与IO操作时性能会更好
  ByteBuffer wrap(byte [] array)
  ByteBuffer wrap(byte [] array, int offset, int length)
//把一个byte数组或byte数组的一部分包装成ByteBuffer。

2.
ByteBuffer定义了一系列get和put操作来从中读写byte数据,如下面几个:
  byte get()
  ByteBuffer get(byte [] dst)
  byte get(int index)
  ByteBuffer put(byte b)
  ByteBuffer put(byte [] src)
  ByteBuffer put(int index, byte b) 
    这些操作可分为绝对定位和相对定为两种,相对定位的读写操作依靠position来定位Buffer中的位置,并在操
  作完成后会更新position的值。在其它类型的buffer中,也定义了相同的函数来读写数据,唯一不同的就是一
  些参数和返回值的类型。

3. 除了读写byte类型数据的函数,ByteBuffer的一个特别之处是它还定义了读写其它primitive数据的方法,如:

  int getInt()             //从ByteBuffer中读出一个int值。
  ByteBuffer putInt(int value)  // 写入一个int值到ByteBuffer中。

  3.1 字节序

    读写其它类型的数据牵涉到字节序问题,ByteBuffer会按其字节序(大字节序或小字节序)写入或读出一个其它
  类型的数据(int,long…)。字节序可以用order方法来取得和设置:
  ByteOrder order() //返回ByteBuffer的字节序。
  ByteBuffer order(ByteOrder bo)   // 设置ByteBuffer的字节序。

  3.2 ByteOrder
    用来表示ByteBuffer字节序的类,可将其看成java中的enum类型。主要定义了下面几个static方法和属性:
    ByteOrder BIG_ENDIAN       代表大字节序的ByteOrder。
    ByteOrder LITTLE_ENDIAN 代表小字节序的ByteOrder。
    ByteOrder nativeOrder()       返回当前硬件平台的字节序。

 

4.
ByteBuffer另一个特别的地方是可以在它的基础上得到其它类型的buffer。如:
  CharBuffer asCharBuffer()
    为当前的ByteBuffer创建一个CharBuffer的视图。在该视图buffer中的读写操作会按照ByteBuffer的字节
  序作用到ByteBuffer中的数据上。

    用这类方法创建出来的buffer会从ByteBuffer的position位置开始到limit位置结束,可以看作是这段数据
  的视图。视图buffer的readOnly属性和direct属性与ByteBuffer的一致,而且也只有通过这种方法,才可
  以得到其他数据类型的direct buffer。

Buffer的创建

ByteBuffer buffer = ByteBuffer.allocate(10);

buffer的创建是通过静态方法allocate,且需传入一个整形参数来指定Buffer容器所能容纳的元素个数。底层实际上是维护着一个长度为10(参数值)的字节数组。且为相关的属性赋上初值:capacity=10,limit=10,position=0。

因为buffer刚创建,且一共能容纳10个元素,也就是0~9的索引位置。因此其position为0(第一个可写入的位置索引),limit为10(因为0~9还未有数据,都是可写入的),capacity为10(一共能容纳的元素个数)。

图片 1

属性位置图(初始)

基本类型长度

在Java中有很多的基本类型,比如:

  • byte,一个字节是8位bit,也就是1B
  • short,16位bit,也就是2B
  • int,32位bit,也就是4B
  • long, 64位bit,也就是8B
  • char,16位bit,也就是2B
  • float,32位bit,也就是4B
  • double,64位bit,也就是8B

不同的类型都会按照自己的位数来存储,并且可以自动进行转换提升。
bytecharshort都可以自动提升为int,如果操作数有long,就会自动提升为longfloatdouble也是如此。

ByteBuffer内部字段

操作方式

对Buffer的读写操作方式主要有2种:随机访问、顺序访问

随机访问:此种方式与操作数组类似,通过下标索引来操作Buffer。比如,指定数据插入到buffer某个索引上的位置;从某个索引位置上读取数据。这种对数据的读写方式不会改变Buffer的三个属性值

顺序访问:此种方式在读写操作时不指定具体的下标索引,而是通过postion与limit指针变量来协同完成对数据的读写。因此这种方式会改变position和limit的索引值。且必须牢记的是,当对同一个Buffer既要进行写入操作又要进行读取操作,在读写操作转换的过程中需要改变position和limit的位置,否则极易出现数据操作混乱。

大端小端

由于一个数据类型可能有很多个字节组成的,那么它们是如何摆放的。这个是有讲究的:

  • 大端:低地址位 存放 高有效字节
  • 小端:低地址位 存放 低有效字节

举个例子,一个char是有两个字节组成的,这两个字节存储可能会显示成如下的模样,比如字符a:

              低地址位    高地址位
大端;        00              96
小端:        96              00

byte[] buff

buff即内部用于缓存的数组。

写操作

随机访问:指定将数据写入到buffer某个具体索引的位置上,一般情况下不推荐此方式,因为这容易造成读写混乱。例如:buffer.put(3,
 (byte)’a’),各个属性位置图如下:

图片 2

随机访问

顺序访问:首先会找到Buffer中position的位置,然后从该位置起,每写入一个数据,position值便会加1。例如:buffer.put((byte)’a’),各个属性位置如如下:

图片 3

顺序访问

当往buffer里面填充8个数据, for(int i = 0; i < 7; i++) {
 buffer.put((byte)’a’);   },此时位置图变为如下:

图片 4

顺序访问

String与new String的区别

再说说"hello"new String("hello")的区别:

如果是"hello",JVM会先去共享的字符串池中查找,有没有"hello"这个词,如果有直接返回它的引用;如果没有,就会创建这个对象,再返回。因此,"a"+"b"相当于存在3个对象,分别是"a""b""ab"

new String("hello"),则省去了查找的过程,直接就创建一个hello的对象,并且返回引用。

position

当前读取的位置。

读/写操作的当前下标。当使用buffer的相对位置进行读/写操作时,读/写会从这个下标进行,并在操作完成后,
buffer会更新下标的值。

读操作

顺序访问:首先会找到Buffer中position的位置,然后从该位置起,每读一个数据,position的值便会加1。如上图,若要从Buffer读取之前写入的数据,首先需要改变position和limit的索引值,这边使用flip方法,flip将limit的位置重新指向position所在的位置,然后position的位置重置为0。
如下图

图片 5

顺序访问

从以上图示可看出,flip方法只是改变position和limit的索引值,并没有清空底层数组的数据。当然,在下次写入的时候,会将旧数据覆盖掉。

读写数据

在直接内存中,通过allocateDirect(int byte_length)申请直接内存。这段内存可以理解为一段普通的基于Byte的数组,因此插入和读取都跟普通的数组差不多。

只不过提供了基于不同数据类型的插入方法,比如:

  • put(byte) 插入一个byte
  • put(byte[]) 插入一个byte数组
  • putChar(char) 插入字符
  • putInt(int) 插入Int
  • putLong(long) 插入long

等等….详细的使用方法,也可以参考下面的图片:

图片 6

对应读取数据,跟写入差不多:

图片 7

注意所有没有index参数的方法,都是按照当前position的位置进行操作的。

下面看看什么是position,还有什么其他的属性吧!

mark

为某一读过的位置做标记,便于某些时候回退到该位置。

一个临时存放的位置下标。调用mark()会将mark设为当前的position的值,以后调用reset()会将position属性设
置为mark的值。mark的值总是小于等于position的值,如果将position的值设的比mark小,当前的mark值会被抛弃掉。

Marking and resetting

buffer提供的mark()和reset()方法是搭配使用的。mark给当前position所在的位置打上一个标记,当进行读写操作之后,position位置会往后移动,在未来的某个时刻,可通过调用reset方法,让position重新回到之前打标记的位置。比如:

图片 8

mark与reset

刚创建好的buffer,mark是未定义的。

对buffer做过mark标记之后,当position或者limit的值被重新设置,且小于mark标记的值时,此mark将会被丢弃。

若未对Buffer做过mark标记,而去调用reset方法,会抛异常。

基本的属性值

它有几个关键的指标:

mark-->position-->limit-->capacity

另外,还有remaining=limit-position

先说说他们的意思吧!

capacity

初始化时候的容量。

这个Buffer最多能放多少数据。capacity一般在buffer被创建的时候指定。

clear and flip and rewind

clear:position置为0,limit置为capacity,mark丢弃,不清空数据。

flip:limit置为position所在的位置,position置为0,mark丢弃,不清空数据。

rewind:position置为0,mark丢弃,不清空数据。

当前位置——position

position是当前数组的指针,指示当前数据位置。举个例子:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.putChar('a');
System.out.println(buffer);
buffer.putChar('c');
System.out.println(buffer);
buffer.putInt(10);
System.out.println(buffer);

由于一个char是2个字节,一个Int是4个字节,因此position的位置分别是:

2,4,8

注意,Position的位置是插入数据的当前位置,如果插入数据,就会自动后移。
也就是说,如果存储的是两个字节的数据,position的位置是在第三个字节上,下标就是2。

java.nio.DirectByteBuffer[pos=2 lim=1024 cap=1024]
java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
java.nio.DirectByteBuffer[pos=8 lim=1024 cap=1024]

position可以通过position()获得,也可以通过position(int)设置。

//position(int)方法的源码
public final Buffer position(int newPosition) {
        if ((newPosition > limit) || (newPosition < 0))
            throw new IllegalArgumentException();
        position = newPosition;
        if (mark > position) mark = -1;
        return this;
    }

注意:position的位置要比limit小,比mark大

limit

在Buffer上进行的读写操作都不能越过这个下标。当写数据到buffer中时,limit一般和capacity相等,当读数据时,
limit代表buffer中有效数据的长度。

读写的上限,limit<=capacity。

 

这些属性总是满足以下条件:
  0 <= mark <= position <= limit <= capacity

limit和position的值除了通过limit()和position()函数来设置,也可以通过下面这些函数来改变:

Buffer clear()
  把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。

Buffer flip()
  把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。

Buffer rewind()
  把position设为0,limit不变,一般在把数据重写入Buffer前调用。

compact()

  该方法的作用是将 position 与
limit之间的数据复制到buffer的开始位置,复制后 position  = limit
-position,limit = capacity

  但如果position 与limit 之间没有数据的话发,就不会进行复制
 详细参考:java nio Buffer 中
compact的作用

mark()与reset()方法

  通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:

  1.buffer.mark();

  2.//call buffer.get() a couple of times, e.g. during parsing.

  3.buffer.reset(); //set position back to mark

equals()与compareTo()方法

  可以使用equals()和compareTo()方法两个Buffer。

  equals()

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

  1. 有相同的类型(byte、char、int等)。
  2. Buffer中剩余的byte、char等的个数相等。
  3. Buffer中所有剩余的byte、char等都相同。

  如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。

  compareTo()方法

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

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

Buffer对象有可能是只读的,这时,任何对该对象的写操作都会触发一个ReadOnlyBufferException。
isReadOnly()方法可以用来判断一个Buffer是否只读。

 

compact

compact方法的主要作用:当一个buffer还有未读数据时,却想对其进行写入操作。这样会把未读的数据给覆盖到。因此,利用compact方法可以将未读的数据进行备份,再进行写入操作。

调用compact方法会执行以下步骤:

将position到limit位置上的数据拷贝并覆盖到buffer底层数组从0开始,长度为limit-position的位置上。

position的位置置为limit-position(还未重置之前的值),limit置为capacity的位置。

mark置为-1,也就是丢弃mark。

如下图:

图片 9

compact前

图片 10

compact后

空间容量——capacity

capacity是当前申请的直接内存的容量,它是申请后就不会改变的。

capacity则可以通过capacity()方法获得。

图解

Heap Buffer与Direct Buffer

在nio中,主要有两种类型的Buffer:heap buffer(堆内缓冲)、direct
buffer(直接缓冲或者堆外缓冲),通过以下两个方法分别生成这两种类型的Buffer对象。

ByteBuffer heapBuffer = ByteBuffer.allocate(10);//堆内缓冲

ByteBuffer directBuffer = ByteBuffer.allocateDirect(10);//直接缓冲

无论是heap buffer还是direct buffer,其buffer对象都是在java堆内的;heap
buffer 底层维护着是字节数组,数据也是存储在java堆内的;direct
buffer在java堆内的buffer对象会维护一个long类型的成员变量,此变量会指向堆外内存,而堆外内存区域实际上维护着buffer存储的真实字节数据。

当buffer对象涉及到I/O操作时,对于HeapByteBuffer来说,其数据是存在java堆内的,而I/O操作属于系统调用,因此操作系统需要新开辟内存空间,然后将java堆内的数据拷贝到新开辟的空间中,再进行相关I/O操作。对于DirectByteBuffer来说,由于其数据本身就是存在于堆外内存,也就是操作系统直接管理的内存,那么,其进行I/O操作时就无需进行数据拷贝,因此会相应的提高性能。

限制大小——limit

我们可能想要改变这段直接内存的大小,因此可以通过一个叫做Limit的属性设置。

limit则可以通过limit()获得,通过limit(int)进行设置。

注意limit要比mark和position大,比capacity小。

//limit(int)方法的源码
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;
    }

put

写模式下,往buffer里写一个字节,并把postion移动一位。写模式下,一般limit与capacity相等。

 图片 11

Read-only buffer

所有的buffer都是可读的,但并不是所有的Buffer都是可写的。判断Buffer是否为只读的,通过调用isReadOnly()方法。

只读Buffer:只能进行Buffer数据内容的读取,无法进行写操作。但是,其position、limit、mark值是可以改变的。

标记位置——mark

mark,就是一个标记为而已,记录当前的position的值。常用的场景,就是记录某一次插入数据的位置,方便下一次进行回溯。

  • 可以使用mark()方法进行标记,
  • 使用reset()方法进行清除,
  • 使用rewind()方法进行初始化

    //mark方法标记当前的position,默认为-1
    public final Buffer mark() {
    mark = position;
    return this;
    }
    //reset方法重置mark的位置,position的位置,不能小于mark的位置,否则会出错
    public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
    }
    //重置mark为-1.position为0
    public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
    }
    

    使用案例

    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    buffer.putChar('a');
    buffer.putChar('c');
    System.out.println("插入完数据 " + buffer);
    buffer.mark();// 记录mark的位置
    buffer.position(30);// 设置的position一定要比mark大,否则mark无法重置
    System.out.println("reset前 " + buffer);
    buffer.reset();// 重置reset ,reset后的position=mark
    System.out.println("reset后 " + buffer);
    buffer.rewind();//清除标记,position变成0,mark变成-1
    System.out.println("清除标记后 " + buffer);
    

    可以看到如下的运行结果:

    插入完数据 java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
    reset前 java.nio.DirectByteBuffer[pos=30 lim=1024 cap=1024]
    reset后 java.nio.DirectByteBuffer[pos=4 lim=1024 cap=1024]
    清除标记后 java.nio.DirectByteBuffer[pos=0 lim=1024 cap=1024]
    

flip

写完数据,需要开始读的时候,将postion复位到0,并将limit设为当前postion。 

Thread safety

buffer并不是线程安全的,当多个线程同时使用一个Buffer时,需要进行同步控制。

剩余空间——remaing

remaing则表示当前的剩余空间:

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

get

从buffer里读一个字节,并把postion移动一位。上限是limit,即写入数据的最后位置。

图片 12

读写实践

写操作主要就是按照自己的数据类型,写入到直接内存中,注意每次写入数据的时候,position都会自动加上写入数据的长度,指向下一个该写入的起始位置:

下面看看如何写入一段byte[]或者字符串:

ByteBuffer buffer = ByteBuffer.allocateDirect(10);
byte[] data = {1,2};
buffer.put(data);
System.out.println("写byte[]后 " + buffer);
buffer.clear();
buffer.put("hello".getBytes());
System.out.println("写string后 " + buffer);

输出的内容为:

写byte[]后 java.nio.DirectByteBuffer[pos=2 lim=10 cap=10]
写string后 java.nio.DirectByteBuffer[pos=5 lim=10 cap=10]

读的时候,可以通过一个外部的byte[]数组进行读取。由于没有找到直接操作直接内存的方法:
因此如果想在JVM应用中使用直接内存,需要申请一段堆中的空间,存放数据。

如果有更好的方法,还请留言。

ByteBuffer buffer = ByteBuffer.allocateDirect(10);
buffer.put(new byte[]{1,2,3,4});
System.out.println("刚写完数据 " +buffer);
buffer.flip();
System.out.println("flip之后 " +buffer);
byte[] target = new byte[buffer.limit()];
buffer.get(target);//自动读取target.length个数据
for(byte b : target){
    System.out.println(b);
}
System.out.println("读取完数组 " +buffer);

输出为

刚写完数据 java.nio.DirectByteBuffer[pos=4 lim=10 cap=10]
flip之后 java.nio.DirectByteBuffer[pos=0 lim=4 cap=10]
1
2
3
4
读取完数组 java.nio.DirectByteBuffer[pos=4 lim=4 cap=10]

clear

将position置为0,并不清除buffer内容。 

mark相关的方法主要是mark()(标记)和reset()(回到标记).

常用方法

上面的读写例子中,有几个常用的方法:

clear()

这个方法用于清除mark和position,还有limit的位置:

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

flip()

这个方法主要用于改变当前的Position为limit,主要是用于读取操作。

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

compact()

这个方法在读取一部分数据的时候比较常用。

它会把当前的Position移到0,然后position+1移到1。

public ByteBuffer compact() {
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);

        unsafe.copyMemory(ix(pos), ix(0), rem << 0);
        position(rem);
        limit(capacity());
        discardMark();
        return this;
    }

比如一段空间内容为:

123456789

当position的位置在2时,调用compact方法,会变成:

345678989

isDirect()

这个方法用于判断是否是直接内存。如果是返回true,如果不是返回false。

rewind()

这个方法用于重置mark标记:

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

参考

1 Java基本数据类型
2 Java中大端与小端

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图