有一些个人理解,大家辩证地看,有问题的地方,还请大家指出。
Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
1.Buffer的基本用法
使用Buffer读写数据一般遵循以下几个步骤:
- 向Buffer写数据
- 调用filp()方法,该方法的作用是将Buffer从写模式切换到读模式,后面会具体介绍
- 从Buffer中读取数据
- 调用clear()方法或是compact()方法,这两个方法的作用主要是清空缓冲区,其实并不是真的清空缓冲区的数据,只是更改了几个标志位,后面会具体介绍
一个栗子:
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put((byte)4);
buffer.flip();
for(int i=0;i<buffer.limit();i++){
System.out.println(buffer.get());
}
Buffer的几个重要成员变量
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:
字面意思理解,这个变量指的是容量,我们可以把它理解为Buffer的容量,也就是我们最多可以给Buffer写入capacity大小的数据
position:
position表示当前读取(或是写入)的位置,写入(或是读取)一个数据后,position会指向下一个可读(或是可写)的位置。
limit:
limit表示我们最多可以读到(或写到)limit位置,这个变量容易和capacity混淆。
这个变量出现的原因主要是我们读和写都用的是一块内存,所以需要一个标记,来只是我们最多可以读到(或写到)什么位置。
mark:
这个变量是一个记号,用来记住当前的position,之后介绍mark()方法的时候会具体介绍。
3.Buffer的类型
Buffer主要有以下一种类型,我们主要会以ByteBuffer为主介绍。
4.Buffer的分配
堆中分配:
ByteBuffer buffer = ByteBuffer.allocate(48);
我们来看一下allocate(int capacity)的代码:
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
我们可以看到,首先做了基本的校验,然后就return了一个HeapByteBuffer的对象。
我们继续看一下HeapByteBuffer的构造方法:
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 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;
}
我们可以看到:这两个方法都是包访问权限,也就是只有java.nio包中的类可以调用(直白地说,就是我们是不能直接调用的),所以这个allocate(int capacity)是一个工厂方法!
ByteBuffer中调用的是HeapByteBuffer的构造方法,也是就在JVM堆中分配内存,所以我们调用allocate(int capacity)得到的是普通的对象。
直接内存中分配:
我们还可以在直接内存中分配ByteBuffer对象:
ByteBuffer buffer = ByteBuffer.allocateDirect(48);
我们来看一下是怎么分配的:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
再点进去看一下DirectByteBuffer(int cap):
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;
}
我们可以看到它是通过调用unsafe的allocateMemory(long size)来分配内存的,我们由unsafe这个名字就可以知道,它是危险的,它使JVM 和操作系统内核可以交流。
unsafe的allocateMemory(long size)是一个本地方法,我们暂时看不到了。
但是,我们可以知道,allocateMemory(long size)分配的是直接内存,也就是JVM堆外内存,而allocate(int capacity)分配的是堆内内存。
为什么要支持分配直接内存呢?这个问题从网络分层来说起吧:
我们的TCP/IP模型,将网络分为了几大层:应用层、传输层、网络层和链路层。其中,传输层(包括传输层)以下是运行在内核空间的,应用层是运行在用户空间的。我们要用通过网络接收数据,要将内核空间的数据复制要用户空间;同样地,要通过网络发送数据,要将用户空间的数据复制到内核空间,再进行发送。
这样复制来复制去效率未免很低,于是我们可以通过直接分配直接内存的方式,来减少一次复制。
但是这个直接内存用起来是有一些注意事项的,后面我会专门写一篇文章来总结这个。
4.几个方法介绍
flip()方法
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
position为写的数据的位置,表示就写了这么多的数据,我们读当然不能超过它啦,于是就将limit设置为position,我们读要从头开始读,就将position置为0。
rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear()方法
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()来完成。
clear()方法将position置为0,limit置为capacity,mark置为-1,也就是将一切置为了原始状态。我们可以看到,它并没有真正地清空缓冲区的数据,只是将标志位还原。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
compact()方法
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素的后面。
mark()方法
这个方法是与下面的reset()方法配套使用的,这个方法很简单,只是将当前的position记录在mark上
public final Buffer mark() {
mark = position;
return this;
}
reset()方法
这个方法就是将position恢复为之间记录下来的值
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();//必要的校验
position = m;
return this;
}