Java NIO - Buffer

声明:java

本文由Yasin Shaw原创,首发于我的网站yasinshaw.com和公众号"xy的技术圈"。git

若是须要转载请联系我(微信:yasinshaw)并在文章开头显著的地方注明出处github

关注公众号便可获取学习资源或加入技术交流群数组

结构

Buffer是“缓冲区”的意思。在Java NIO中,全部的数据都要通过Buffer,下图是Buffer内部的基本结构。微信

buffer

它其实就是一个数组,里面有三个指针:position, limit, capacity。socket

capacity

capacity为这个数组的容量,是不可变的。学习

limit

limit是Buffer中第一个不可读写的元素的下标,也即limit后的数据不可进行读写。limit不能为负,也不能大于capacity。网站

limit初始的时候是与capacity值是同样的。this

position

position表示下一个元素即将读或者写的下标。position不能为负也不能大于limit。position初始的时候为0。spa

类关系

Buffer是一个抽象类,它有许多子抽象类,对应7种Java的基本类型(除了boolean)。以下图:

image.png

ByteBuffer为例,它有两种实现,一种是HeapByteBuffer,另外一种是DirectByteBuffer,分别对应堆内存直接内存

堆内存会把这个对象分配在JVM堆里,就跟普通对象同样。而直接内存又被称为堆外内存,在使用IO的时候,咱们更推荐使用直接内存。

为何推荐使用直接内存呢?其实这跟JVM的垃圾回收机制有关。IO每每会占用一个比较大的内存空间,若是分配到JVM堆里面,会被认为是一个大对象,影响JVM垃圾回收效率。

堆外内存若是满了(达到系统内存的界限),也会抛出OOM异常。

初始化

Buffer有什么用?Buffer通常是与Channel配合起来用,Channel读数据的时候,会先读到Buffer里,写数据的时候,也会先写到Buffer里。

下面介绍一下具体是怎么使用Buffer的。

通常来讲,是直接使用第二级类,好比ByteBuffer。它们有两个工厂方法allocateallocateDirect,用于初始化和申请内存。前面提到了在操做IO时,一般使用直接内存,因此通常是这样初始化:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
复制代码

能够用isDirect()方法来判断当前Buffer对象是否使用了直接内存。

写数据

往Buffer中写数据主要有两种方式:

  • 从Channel写到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字节,那写入数据之后三个指针就是这样的:

  • position = 16
  • limit = 1024
  • capacity = 1024

切换模式

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 = 0
  • limit = 16
  • capacity = 1024

读数据

与写数据对应,读数据也有两种方式:

  • 从Buffer读到Channel
  • 从Buffer读到数组

读数据会从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还有其它一些操做那三个指针的方法,不过使用频率没有上述方法高,因此本文不作详细介绍,感兴趣的读者能够去看一下源码。

使用

这里贴一下读和写的使用的案例代码:

从字符串到Channel:

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 从字符串写到Buffer
buffer.put("hi, 这是client".getBytes(StandardCharsets.UTF_8));
buffer.flip(); // 转换模式
// 从Buffer写到Channel
socketChannel.write(buffer);
复制代码

从Channel到字符串:

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);
}
复制代码

完整示例代码

相关文章
相关标签/搜索