简介
ByteBuffer在NIO通信中负责数据读写,本质就是个固定长度的byte数组,
上图描述了ByteBuffer父子类的关联关系,从Buffer衍生下来的buffer类包括:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer,LongBuffer,ShortBuffer,MappedByteBuffer。而ByteBuffer的实现类包括"HeapByteBuffer"和"DirectByteBuffer"两种。用的比较多,所以今天主要讲这几个。
属性接口
final byte[] hb; // Non-null only for heap buffers
final int offset;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- capacity
缓冲区能够容纳的数据元素的最大数量。这个容量在缓冲区创建时被设定,不能为负并且永远不能被改变。 - limit
第一个不应该读取或写入的元素的索引,即执行get()、put操作数据范围不可超过limit值。缓冲区的限制不能为负,并且不能大于其容量。 - position
下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。 - mark
用于备份当前的position。调用mark( )把当前的position赋值给mark,mark=postion。调用reset( )把mark值还原给position,position=mark。标记在设定前是未定义的默认:-1。结合reset(),mark将会跳转到上次标记的位置,循环的读取内容。
这四个属性之间总是遵循以下关系: 0 <= mark <= position <= limit <= capacity - allocate
初始化字节数组hd,在虚拟机堆上申请内存空间,分配一个新的字节缓冲区(HeapByteBuffer)。新缓冲区的位置(position)将为零,其界限(limit)将为其容量(capacity),其标记(mark)默认:-1。它将具有一个底层实现数组,且其数组偏移量将为零。源码如下:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
//此HeapByteBuffer构造方法如下
HeapByteBuffer(int cap, int lim) {
//mark:-1,position:0,limit:lim,capacity:cap,hb:new byte[cap],offset:0
super(-1, 0, lim, cap, new byte[cap], 0);
}
- allocateDirect
分配新的直接字节缓冲区(DirectByteBuffer)。新缓冲区的位置(position)将为零,其界限(limit)将为其容量(capacity),其标记(mark)默认:-1。
尽管该方法的内部逻辑仅一行new DirectByteBuffer(capacity) ,但DirectByteBuffer的构造方法相较于HeapByteBuffer却复杂的多。通过unsafe.allocateMemory申请堆外内存,并在ByteBuffer的address变量中维护指向该内存的地址。unsafe.setMemory(base, size, (byte) 0)方法把新申请的内存数据清零。不仅对象构建耗时较HeapByteBuffer更长,而且脱离了JVM正常gc管理,用完之后需要手动释放或者触发full gc回收
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
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;
}
JVM底层的执行数据读取/发送操作的都是通过DirectByteBuffer。假如我们在应用层将消息封装在HeapByteBuffer中,执行write方法后,JVM会将该对象中的内容拷贝到DirectByteBuffer再执行输出,在这个过程中多了一次内存拷贝。如若开始我们就将数据写入DirectByteBuffer中,将获得更好的性能表现。对于DirectByteBuffer本身存在的创建耗时与对象回收问题,可以依托于内存池技术得以解决。
- wrap
该方法执行的是 new HeapByteBuffer(array, offset, length); 操作。与allocate方法的差异在于byte数组是由外部指定的。 - put
将给定的字节写入此缓冲区的当前位置(position),然后该位置递增。以HeapByteBuffer实现算法为例
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
protected int ix(int i) {
return i + offset;
}
- get
读取此缓冲区当前位置的字节,然后该位置递增。以HeapByteBuffer实现算法为例
public byte get(int i) {
return hb[ix(checkIndex(i))];
}
final int checkIndex(int i) { // package-private
if ((i < 0) || (i >= limit))
throw new IndexOutOfBoundsException();
return i;
}
protected int ix(int i) {
return i + offset;
}
- filp
反转此缓冲区。首先将限制设置为当前位置,然后将位置设置为 0。设置mark为-1。
通常运用于ByteBuffer从写模式切换到读模式时使用。如下图,写模式下往ByteBuffer写入“hello”,position值为5;此时要从ByteBuffer中取出这些数据,需要执行flip将状态切换到读模式,并限制读取数据的范围limit为当初写入的结尾边界5,设置读位置position为0。之后再执行读操作get的时候,position值递增,但不可操作limit。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
- compact
将缓冲区的当前位置和界限之间的字节(如果有)复制到缓冲区的开始处。即将索引 p = position() 处的字节复制到索引 0 处,将索引 p + 1 处的字节复制到索引 1 处,依此类推,直到将索引 limit() - 1 处的字节复制到索引 n = limit() - 1 - p 处。然后将缓冲区的位置设置为 n+1,并将其界限设置为其容量。
public ByteBuffer compact() {
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining());
limit(capacity());
discardMark();
return this;
}
其实只是数据移位。结合上面flip的例子,当position读到位置2的时候,我们想再次切换到写模式,就可以使用compact。如下图所示,原本ByteBuffer中还有三个byte:[L,L,O]可读,执⾏compact会将这三个byte拷贝到区间[0,2]位置,之后的[3,5]区间为可写空间,执行ByteBuffer.put('H')会填充position:3。
- clear
清除此缓冲区,但不会擦除数据。将位置设置为 0,将限制(limit)设置为容量(capacity),设置mark为-1。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
样例
public static void main(String[] args) {
byte[] data = "test-nio".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(8);
printBuffer("初始", buffer);
buffer.put(data);
printBuffer("写数据", buffer);
buffer.flip();
printBuffer("执行flip", buffer);
byte[] read = new byte[buffer.remaining()];
buffer.get(read);
printBuffer("读数据", buffer);
System.out.println(new String(read));
System.out.println("-----------------");
//采用wrap方式构建的初始为读模式。先读取部分内容“test-”,执行compact进行数据移位后再读取剩余部分内容
buffer = ByteBuffer.wrap(data);
printBuffer("初始", buffer);
byte[] read1 = new byte[6];
buffer.get(read);
printBuffer("读取\"test-\"", buffer);
buffer.compact();
printBuffer("执行compact", buffer);
read = new byte[buffer.remaining()];
buffer.get(read);
printBuffer("读取数据", buffer);
System.out.println(new String(read));
}
private static void printBuffer(String title, ByteBuffer buffer) {
System.out.println(title + " position:" + buffer.position() + " ,limit:" + buffer.limit() + " ,capacity:"
+ buffer.capacity());
}
输出
初始 position:0 ,limit:8 ,capacity:8
写数据 position:8 ,limit:8 ,capacity:8
执行flip position:0 ,limit:8 ,capacity:8
读数据 position:8 ,limit:8 ,capacity:8
test-nio
-----------------
初始 position:0 ,limit:8 ,capacity:8
读取"test-" position:8 ,limit:8 ,capacity:8
执行compact position:0 ,limit:8 ,capacity:8
读取数据 position:8 ,limit:8 ,capacity:8
test-nio
NIO的数据访问方式
NIO提供了比传统的文件访问方式更好的方法。一个是FileChannel.transferTo
、FileChannel.transferFrom
和FileChannel.map
FileChannel.transferXX
减少数据从内核到用户空间的复制,数据直接在内核空间中移动,在Linux中使用sendfile系统调用。
FileChannel.map
将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如大文件的MD5校验。这种方式和操作系统底层I/O实现相关。