NIO 看破也说破(五): 搞,今天就搞,搞懂 Buffer

用户头像
小眼睛聊技术
关注
发布于: 2020 年 06 月 04 日
NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer

前言



Java NIO 中的三件法宝:ChannelSelectorBuffer 。前面几节中,我们花了很大篇幅讲过 Selector ,咱们今天只搞 Buffer 。希望能通过本文搞明白 Buffer 的基本用法和原理。



掌握重点:

  1. 两个重要指针不停变换

  2. 一块 Buffer 可读可写

  3. 基本操作的 api 用法

  4. ByteBuffer 可以在 JVM 堆外分配直接内存

基本操作



上一篇我们模拟 client 发送请求的时候代码如下:

InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
System.out.printf("接到服务端响应:%s,处理了%d\r\n", br.readLine(), (System.currentTimeMillis() - start));
br.close();
inputStream.close();

在普通 BIO 模式下,我们只能自己维护一个 byte 数组或者是 char 数组来进行批量读写,或者使用 BufferedReaderBufferedInputStream 来做读写缓冲区。

buffer.clear();
buffer.put(("收到,你发来的是:" + sb + "\r\n").getBytes("utf-8"));
buffer.flip();

Java NIO Buffer 用于和 NIO Channel 交互,我们从Channel 中读取数据到 Buffer 里,从 Buffer 把数据写入到 Channel。本质上,就是存在一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被 NIO Buffer 包裹起来,对外提供一系列的读写方便开发的接口

  • 把数据写入 Buffer

  • 调用 flip();

  • Buffer 中读取数据;

  • 调用 clear() 或者 compact()

当写入数据到 Buffer 中时,Buffer 会记录已经写入的数据大小。当需要读数据时,通过 flip() 方法把 Buffer 从写模式调整为读模式;在读模式下,可以读取所有已经写入的数据。

Buffer实现



缓存区,内部使用字节数组存储数据,并维护几个特殊变量,实现数据的反复利用。在 java.nio.Buffer 中定义了4个成员变量:

  1. mark:初始值为 -1,用于备份当前的 position ;

  2. position:初始值为 0,position 表示当前可以写入或读取数据的位置,当写入或读取一个数据后,position 向前移动到下一个位置;

  3. limit:写模式下,limit 表示最多能往 Buffer 里写多少数据,等于 capacity 值;读模式下,limit 表示最多可以读取多少数据。

  4. capacity:缓存数组大小



核心点:对于 Buffer 的操作,就是在不停的变换 position 和 limit 指针的位置,达到定位读取位置和终止位置的目的,从而可以准确的在边界内读取数据。



代码实现:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

以字节缓冲区为例,ByteBuffer 是一个抽象类,不能直接通过 new 语句来创建,只能通过一个 static 方法 allocate 来创建:

ByteBuffer byteBuffer = ByteBuffer.allocate(10);

调用上述语句,相当于创建一个大小为 10 个字节的 ByteBuffer ,此时 mark = -1, position = 0, limit = 10, capacity = 10



我们看一下 Buffer 的常见方法,内部是如何实现的:

put



put 方法是把一个 byte 变量 x 放到缓冲区中,同时 position 会加 1

public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}





一起看一下不停 put 数据时,几个变量的变化:

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte) 'l');
byteBuffer.put((byte) 'o');
byteBuffer.put((byte) 'v');
byteBuffer.put((byte) 'e');
System.out.println(byteBuffer.limit()); // 结果10
System.out.println(byteBuffer.position());// 结果4
System.out.println(byteBuffer.capacity());// 结果10
byteBuffer.put((byte) ' ');
byteBuffer.put((byte) 'x');
byteBuffer.put((byte) 'y');
byteBuffer.put((byte) 'j');
System.out.println(byteBuffer.limit());// 结果10
System.out.println(byteBuffer.position());// 结果8
System.out.println(byteBuffer.capacity());// 结果10



get



get 方法,是从 position 的位置去取缓冲区中的一个字节

public byte get() {
return hb[ix(nextGetIndex())];
}
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}



flip



如果想在一个 Buffer 中放入了数据,然后想从中读取的话,就要把 position 调到我想读的那个位置才行,同时需要调整 limit。

byteBuffer.limit(byteBuffer.position())
byteBuffer.position(0);





Java 中把这两步操作,封装在一个 flip 方法中:

public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}



mark



mark 就很容易理解了,它就是记住当前的位置用的

public final Buffer mark() {
mark = position;
return this;
}

在调用过 mark 以后,再进行缓冲区的读写操作,position 就会发生变化,为了再回到当初的位置,我们可以调用 reset 方法恢复position 的值:

public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}

clear



Buffer 中特殊的4个变量初始为原始值

public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}



回顾一下核心点:对于 Buffer 的操作,就是在不停的变换 position 和 limit 指针的位置,达到定位读取位置和终止位置的目的,从而可以准确的在边界内读取数据。

Direct Buffer



在创建 ByteBuffer 是我们是采用的静态方法直接 allocate 得到一个 buffer 对象:

ByteBuffer buf = ByteBuffer.allocate(1024);

在 JVM 中,创建的对象是放入在堆中。比如,当我们 Object o = new Object() 时,会在堆内存上分配一块内存空间给 new Object() ,在栈空间上持有引用 o 保存 Object 的内存地址 。JVM 做垃圾回收,会把堆中的对象,在不同的分区中来回拷贝。内存地址会频繁发生变化,本身 Buffer 会频繁读写,这样会导致内存整理繁琐。有没有办法脱离JVM对象管理呢?在创建 Buffer 的静态方法中还有一个方法:

ByteBuffer buf = ByteBuffer.allocateDirect(1024);

我们来比对一下方法的实现:

public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}

调用 allocate() 创建了一个 HeapByteBuffer ,调用 allocateDirect() 创建的是 DirectByteBuffer 。看名字很直观的表达,一个是「堆」内存,一个是「直接」内存。



看一下 DirectByteBuffer 的实现:

// Primary constructor
//
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 来分配内存,而 allocateMemory 是一个 native 方法,会调用 malloc 方法在 JVM 外分配一块内存空间。



总之,这里在 Java 堆外申请了一块内存,并把这个内存的地址记录下来。以后要是再使用这个ByteBuffer的话,就会直接访问从address开始的那一段内存。



DirectBuffer 一个直观的优点是不被 GC 管理,所以发生 GC 的时候,整理内存的压力就会小。当然,它并不是完全不被 GC 管理还是能被回收的,但是在 GC 平常整理内存的时候确实是不会去管它。



类结构



我们只是以常见的 ByteBuffer 为例,在 NIO 中还提供了各种类型的Buffer ,这里就不再赘述。

图片源于网络

结论



  1. Buffer 中有两个重要指针 position 和 limit 不停变换位置

  2. 一块 Buffer 可读可写,内部是一个 capacity 大小的数组

  3. 基本操作的 api 用法,put 、get、flip、mark、clear

  4. flip 方法改变了指针 position 和 limit 的位置

  5. 可以在 JVM 堆外分配直接内存



今天就搞到的这里,划的重点需要牢记,Buffer 的操作不注意顺序会出现各种问题。



系列

NIO 看破也说破(一)—— Linux/IO 基础

NIO 看破也说破(二)—— Java 中的两种 BIO

NIO 看破也说破(三)—— 不同的 IO 模型

NIO 看破也说破(四)—— Java 的 NIO

NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer



关注我

如果您在微信阅读,请您点击链接 关注我 ,如果您在 PC 上阅读请扫码关注我,欢迎与我交流随时指出错误



发布于: 2020 年 06 月 04 日 阅读数: 1541
用户头像

小眼睛聊技术

关注

欢迎关注公众号“小眼睛聊技术” 2018.11.12 加入

互联网老兵,关注产品、技术、管理

评论 (5 条评论)

发布
用户头像
good
2020 年 09 月 14 日 13:04
回复
用户头像
真正的深入浅出!很精彩👏
2020 年 06 月 07 日 16:56
回复
谢谢肯定,欢迎转发,推荐,一起讨论
2020 年 06 月 07 日 19:04
回复
用户头像
感谢继续分享这个专题,InfoQ首页推荐。
2020 年 06 月 05 日 09:05
回复
🌹
2020 年 06 月 05 日 10:39
回复
没有更多了
NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer