声明:java
本文由Yasin Shaw原创,首发于我的网站yasinshaw.com和公众号"xy的技术圈"。git
若是须要转载请联系我(微信:yasinshaw)并在文章开头显著的地方注明出处。github
关注公众号便可获取学习资源或加入技术交流群。数组
Buffer是“缓冲区”的意思。在Java NIO中,全部的数据都要通过Buffer,下图是Buffer内部的基本结构。微信
它其实就是一个数组,里面有三个指针:position, limit, capacity。socket
capacity
为这个数组的容量,是不可变的。学习
limit
是Buffer中第一个不可读写的元素的下标,也即limit后的数据不可进行读写。limit不能为负,也不能大于capacity。网站
limit初始的时候是与capacity值是同样的。this
position
表示下一个元素即将读或者写的下标。position不能为负也不能大于limit。position初始的时候为0。spa
Buffer是一个抽象类,它有许多子抽象类,对应7种Java的基本类型(除了boolean
)。以下图:
以ByteBuffer
为例,它有两种实现,一种是HeapByteBuffer
,另外一种是DirectByteBuffer
,分别对应堆内存和直接内存。
堆内存会把这个对象分配在JVM堆里,就跟普通对象同样。而直接内存又被称为堆外内存,在使用IO的时候,咱们更推荐使用直接内存。
为何推荐使用直接内存呢?其实这跟JVM的垃圾回收机制有关。IO每每会占用一个比较大的内存空间,若是分配到JVM堆里面,会被认为是一个大对象,影响JVM垃圾回收效率。
堆外内存若是满了(达到系统内存的界限),也会抛出OOM异常。
Buffer有什么用?Buffer通常是与Channel配合起来用,Channel读数据的时候,会先读到Buffer里,写数据的时候,也会先写到Buffer里。
下面介绍一下具体是怎么使用Buffer的。
通常来讲,是直接使用第二级类,好比ByteBuffer
。它们有两个工厂方法allocate
和allocateDirect
,用于初始化和申请内存。前面提到了在操做IO时,一般使用直接内存,因此通常是这样初始化:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
复制代码
能够用isDirect()
方法来判断当前Buffer对象是否使用了直接内存。
往Buffer中写数据主要有两种方式:
从Channel写到Buffer用的是Channel的read(Buffer buffer)
方法,而从数组写到Buffer,主要用的是Buffer的put
方法。
// 获取Channel里面的数据并写到buffer
// 返回的是读的位置,也就是buffer的position
int readBytes = socketChannel.read(buffer);
// 从byte数组写到Buffer
buffer.put("hi, 这是client".getBytes(StandardCharsets.UTF_8));
复制代码
咱们假设Buffer申请了1024字节,这个字符串占用16字节,那写入数据之后三个指针就是这样的:
Buffer分为读模式和写模式,能够经过flip()
方法转换模式。事实上,查看这个方法源码,发现flip方法也只是对三个指针进行了操做而已。
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
复制代码
mark指针用于reset()方法,若是reset()方法被调用,position就会被重置到mark位置。若是mark没有被定义,调用reset()方法会抛出InvalidMarkException异常。一旦mark被定义,就必定不能为负数,而且小于等于position的位置。
mark()方法的做用至关于能够“暂时记录position”的位置,这样之后能够经过reset()方法回到这个位置。
切换模式后,三个指针变成了这样:
与写数据对应,读数据也有两种方式:
读数据会从position读到limit的位置。
示例代码:
// 读取buffer的数据并写入channel
socketChannel.write(buffer);
// 把buffer里面的数据读到byte数组
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
复制代码
这里用到了Buffer的remaining()
方法。这个方法是告诉咱们须要读多少字节,方法源码:
public final int remaining() {
return limit - position;
}
复制代码
通常来讲,一个Channel用一个Buffer,但Buffer能够重复使用,尤为是对于一些比较大的IO传输内容来讲(好比文件),clear()
与compact()
方法能够重置Buffer。它们有一些微小的区别。
对于clear方法来讲,position将被设回0,limit被设置成 capacity的值。
compact方法将全部未读的数据拷贝到Buffer起始处。而后将position设到最后一个未读元素后面一位。limit属性依然像clear方法同样,设置成capacity。如今Buffer准备好写数据了,可是不会覆盖未读的数据。
通常来讲,用clear方法的场景会多一点。
源码:
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
public ByteBuffer compact() {
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
try {
UNSAFE.copyMemory(ix(pos), ix(0), (long)rem << 0);
} finally {
Reference.reachabilityFence(this);
}
position(rem);
limit(capacity());
discardMark();
return this;
}
复制代码
Buffer还有其它一些操做那三个指针的方法,不过使用频率没有上述方法高,因此本文不作详细介绍,感兴趣的读者能够去看一下源码。
这里贴一下读和写的使用的案例代码:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 从字符串写到Buffer
buffer.put("hi, 这是client".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 转换模式
// 从Buffer写到Channel
socketChannel.write(buffer);
复制代码
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 从Channel写到Buffer
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
buffer.flip(); // 转换模式
byte[] bytes = new byte[buffer.remaining()];
// 从Buffer写到字节数组
buffer.get(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
System.out.println("server 收到:" + body);
}
复制代码