理解 ByteBuffer

ByteBuffer 译为 字节缓冲区 , 是 Java nio 包下提供的一个抽象类 java.nio.ByteBuffer

缓冲区即预先分配的内存,是从内存中提前划分出的一块区域。

直接已知子类是 MappedByteBuffer

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>

直接与非直接内存/字节缓冲区

byte长度 file长度 java java bytebuffer_dying搁浅

给定一个直接内存,Java 虚拟机将尽最大努力在之上直接进行本地 I/O 操作。

也就是说,使用 ByteBuffer 可以避免 直接内存堆内存数据拷贝过程。即常说的 零拷贝

可以使用 allocateDirect 方法来创建直接字节缓冲区。该方法返回的缓冲区通常比堆内存的缓冲区有更高的分配和释放成本

直接缓冲区的内容可能驻留在正常的堆垃圾回收之外,它们对程序内存占用的影响并不明显。通常建议直接缓冲区主要分配给受底层系统 I/O 操作影响较多的,大型、长期存在的数据。仅在直接缓冲区对系统性能优化是可以预见的情况下才进行使用(不要瞎优化,负优化,分配和释放比堆内申请要慢

也可以通过将文件的区域直接 mapping 到内存来创建直接字节缓冲区,字节缓冲区是直接的还是非直接的可以通过调用 isDirect 来确定。

创建 ByteBuffer

ByteBuffer 是一个抽象类,它为我们提供了静态工厂方法来创建实例,如下图

byte长度 file长度 java java bytebuffer_dying搁浅_02

allocation 分配

分配一个具备特定容量的 ByteBuffer

allocate 会在 Java 堆中进行分配,返回的是一个 java.nio.HeapByteBuffer 实例

ByteBuffer buffer = ByteBuffer.allocate(10);

allocateDirect 会在直接内存分配,返回的是一个 java.nio.DirectByteBuffer 实例

ByteBuffer buffer = ByteBuffer.allocateDirect(10);

wrapping 包装

从现有 array 数组进行包装,所有对于 byte[] array 的操作都会反应到 bytebuffer 字节缓冲区中,反之亦然

byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);

上面的代码等价于

ByteBuffer buffer = ByteBuffer.wrap(bytes, 0, bytes.length);

ByteBuffer 四个基本索引

这四个索引记录了底层数据的状态

  • Capacity: 缓冲区可以容纳的最大数据元素个数
  • Limit: 读取或者写入的最大位置
  • Position: 当前读取或者写入的位置
  • Mark: 一个被标记的 position 值 (相当于记录 position 之前的一个状态)

这四个索引的关系是

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

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

这些都是定义在继承的父类 java.nio.Buffer

当我们创建一个 ByteBuffer 实例时,mark 是未定义的初始值 -1,position 是 0 ,limit = capacity 。

例如

ByteBuffer buffer = ByteBuffer.allocate(10);
int position = buffer.position(); // 0
int limit = buffer.limit();       // 10
int capacity = buffer.capacity(); // 10

这容量是只读的,无法修改,可以使用以下方法改变 position 和 limit

buffer.position(2);
buffer.limit(5);

ByteBuffer 索引相关方法

从概念上讲,ByteBuffer 是对字节数组进行了包装,提供了一系列方法对底层的数据进行读写操作,且这一系列的方法高度依赖于维护的索引。

我们可以抽象理解为,ByteBuffer 就是 字节数组 + 额外索引 的一个容器

byte[] array + index

byte长度 file长度 java java bytebuffer_dying搁浅_03

上面是索引相关操作的方法,大体可以分为四部分:

  1. 基本
  2. 标记和重置
  3. 清除、翻转、倒带、压紧
  4. 剩余

标记和重置

mark 方法和 reset 方法时配合使用的,如果没有进行过 mark ,直接执行 reset 将会抛出 InvalidMarkException

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

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

清除、翻转、倒带、压紧

clear 清除,相当于重置为初始状态,如果要重用缓存区使用 clear 方法最方便

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

flip 翻转,限制更新为当前位置,当前位置重置为 0,会从写模式切换为读模式,一般会配合 compact 方法使用

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

rewind 倒带,重置 position 为 0 mark 为 -1 未定义

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

compact 方法会将 limit 置为 容量, position 为变为剩余 limit - position 并重置 mark

剩余

public final int remaining() {
    return limit - position;
}
public final boolean hasRemaining() {
    return position < limit;
}

使用 ByteBuffer 传输数据

ByteBuffer 提供了传输或者获取数据的一系列方法

byte长度 file长度 java java bytebuffer_byte长度 file长度 java_04

关于传输字节数据

public abstract byte get();
public abstract ByteBuffer put(byte b);
public abstract byte get(int index);
public abstract ByteBuffer put(int index, byte b);

提供了两个版本的方法,一个有 index 参数,一个没有。

没有传入 index 的方法会对当前 position 位置的数据元素进行操作,之后 position++

传入 index 则对 index 位置元素进行操作,且不会改变 position