为了账号安全,请及时绑定邮箱和手机立即绑定

Java NIO - Buffer

标签:
Java

结构

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

buffer

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

capacity

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

limit

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

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

position

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

类关系

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);
}

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消