学妹觉得我之前写的Reactor模型还不错,

问我是不是能够再总结一下ByteBuffer

其实平时不怎么会运用ByteBuffer的,

可是架不住学妹一杯奶茶,

那就简略的总结一下吧。


前言

已知NIO中有三大组件:ChannelBufferSelector。那么Buffer的效果便是供给一个缓冲区,用于用户程序和Channel之间进行数据读写,也便是用户程序中能够运用BufferChannel写入数据,也能够运用BufferChannel读取数据。

ByteBufferBuffer子类,是字节缓冲区,特色如下所示。

  1. 巨细不行变。一旦创立,无法改动其容量巨细,无法扩容或许缩容;
  2. 读写灵活。内部经过指针移动来完结灵活读写;
  3. 支撑堆上内存分配和直接内存分配。

本文将对ByteBuffer的相关概念,常用API以及运用事例进行分析。全文约1万字,知识点脑图如下。

ByteBuffer-知识点脑图

正文

一. Buffer

NIO中,八大根底数据类型中除了boolean外,都有相应的Buffer的完结,类图如下所示。

Buffer类图

Buffer类对各种根底数据类型的缓冲区做了顶层笼统,所以要了解ByteBuffer,首先应该学习Buffer类。

1. Buffer的特点

一切缓冲区结构都有如下特点。

特点 阐明
int position 方位索引。代表下一次即将操作的元素的方位,默许初始为0,方位索引最小为0,最大为limit
int limit 约束索引。约束索引及之后的索引方位上的元素都不能操作,约束索引最小为0,最大为capacity
int capacity 容量。缓冲区的最大元素个数,创立缓冲区时指定,最小为0,不能改动

三者之间的巨细联系应该是:0 <= position <= limit <= capacity,图示如下。

网络编程-Buffer特点示意图

除此之外,还有一个特点叫做mark,如下所示。

特点 阐明
int mark 符号索引。mark会符号一个索引,在Buffer#reset调用时,将position重置为markmark不是有必要的,可是当界说mark后,其最小为0,最大为position

关于mark还有如下两点阐明。

  1. positionlimit一旦小于markmark会被丢弃;
  2. 没有界说mark时假如调用了Buffer#reset则会抛出InvalidMarkException

2. Buffer的读形式

Buffer有两种形式,读形式和写形式,在读形式下,能够读取缓冲区中的数据。那么关于一个缓冲区,要读取数据时,分为两步。

  1. 拿到position方位索引;
  2. position方位的数据。

那么Buffer供给了nextGetIndex() 办法和nextGetIndex(int nb) 办法来获取position,先看一下nextGetIndex() 办法的完结。

final int nextGetIndex() {
    // limit方位是不行操作的
    if (position >= limit) {
        throw new BufferUnderflowException();
    }
    // 回来当时position
    // 然后position后移一个方位
    return position++;
}

nextGetIndex() 办法首先校验一下position是否大于等于limit,由于limit及之后的方位都是不行操作的,所以只需满足position大于等于limit则抛出反常,然后回来当时的position(也便是当时可操作的方位),终究position后移一位。

nextGetIndex(int nb) 办法,则是用于Buffer的子类ByteBuffer运用,由于ByteBuffer的一个元素便是一个字节,而假如想要经过ByteBuffer获取一个整形数据,那么此刻就需求连续读取四个字节。nextGetIndex(int nb) 办法如下所示。

final int nextGetIndex(int nb) {
    // 判别一下剩下可操作元素是否够本次获取
    if (limit - position < nb) {
        throw new BufferUnderflowException();
    }
    // 暂存当时position
    int p = position;
    // 然后position后移nb个方位
    position += nb;
    // 回来暂存的position
    return p;
}

拿到position后,实践的读取数据,由Buffer的子类来完结。

3. Buffer的写形式

有读就有写,在Buffer的写形式下,写入数据也是分为两步。

  1. 拿到position方位索引;
  2. 写入数据到position方位。

写形式下,Buffer相同为获取position供给了两个办法,如下所示。

final int nextPutIndex() {
    // limit方位是不行操作的
    if (position >= limit) {
        throw new BufferOverflowException();
    }
    // 回来当时position
    // 然后position后移一个方位
    return position++;
}
final int nextPutIndex(int nb) {
    // 判别一下剩下可操作元素是否够本次写入
    if (limit - position < nb) {
        throw new BufferOverflowException();
    }
    // 暂存当时position
    int p = position;
    // 然后position后移nb个方位
    position += nb;
    // 回来暂存的position
    return p;
}

相同,拿到position后,实践的写入数据,由Buffer的子类来完结。

4. Buffer读写形式切换

Buffer供给了读形式和写形式,同一时间Buffer只能在同一形式下作业,相应的,Buffer供给了对应的办法来做读写形式切换。

首先是读形式切换到写形式,先看如下示意图。

网络编程-Buffer读且读完切换写示意图

上图中的状况是缓冲区中的数据现已悉数被读完,那么此刻假如要切换到写形式,对应的办法是clear() 办法,如下所示。

public final Buffer clear() {
    // 重置position为0
    position = 0;
    // 设置limit为capacity
    limit = capacity;
    // 重置mark为-1
    mark = -1;
    return this;
}

注意,尽管办法名叫做clear(),可是实践缓冲区中的数据并没有被铲除,而只是将方位索引position,约束索引limit进行了重置,一起铲除了符号状态(也便是将mark设置为-1)。切换到写形式后,缓冲区示意图如下所示。

网络编程-Buffer读且读完切换写后示意图

然后是写形式切换到读形式,先看如下示意图。

网络编程-Buffer写切换读前示意图

数据现已写入结束了,此刻假如要切换到读形式,对应的办法是flip(),如下所示。

public final Buffer flip() {
    // 由于position方位还没写入数据
    // 所以将position方位设置为limit
    limit = position;
    // 重置position为0
    position = 0;
    // 重置mark为-1
    mark = -1;
    return this;
}

由于position永远代表下一个可操作的方位,那么在写形式下,position代表下一个写入的方位,那么其实就还没有数据写入,所以调用flip() 办法后,首先将position方位设置为limit,表明数据最多读取到limit的上一个方位,然后重置positionmark。切换到读形式后,缓冲区示意图如下所示。

网络编程-Buffer写切换读后示意图

5. Buffer的rewind操作

在运用Buffer时,能够针对现已操作的区域进行重操作,假定缓冲区示意图如下。

网络编程-Buffer的rewind前示意图

再看一下rewind() 办法的完结,如下所示。

public final Buffer rewind() {
    // 重置position为0
    position = 0;
    // 铲除mark
    mark = -1;
    return this;
}

主要便是将方位索引position重置为0,这样就能从头操作现已操作过的方位了,一起假如启用了mark,那么还会铲除mark,也便是重置mark为-1。rewind() 办法调用后的缓冲区示意图如下所示。

网络编程-Buffer的rewind后示意图

6. Buffer的reset操作

在运用Buffer时,能够启用mark来符号一个现已操作过的方位,假定缓冲区示意图如下。

网络编程-Buffer的reset前示意图

再看一下reset() 办法的完结,如下所示。

public final Buffer reset() {
    int m = mark;
    // 只需启用mark那么mark就不能为负数
    if (m < 0) {
        throw new InvalidMarkException();
    }
    // 将position重置为mark
    position = m;
    return this;
}

在没有启用mark时,mark为-1,只需启用了mark,那么mark就不能为负数。在reset() 中主要便是将方位索引position从头设置到mark符号的方位,以完结对mark符号的方位及之后的方位进行从头操作。reset()  办法调用后的缓冲区示意图如下所示。

网络编程-Buffer的reset后示意图

二. ByteBuffer

在上一节主要对Buffer进行了一个阐明,那么本节会在上一节的根底上,对ByteBuffer及其完结进行学习。

1. ByteBuffer的特点

ByteBuffer相较于Buffer,多了如下三个特点。

特点 阐明
byte[] hb 字节数组。仅HeapByteBuffer会运用到,HeapByteBuffer的数据存储在hb
int offset 偏移量。仅HeapByteBuffer会运用到,后面会具体阐明
isReadOnly 是否只读。仅HeapByteBuffer会运用到,后面会具体阐明

NIO中为ByteBuffer分配内存时,能够有两种办法。

  1. 在堆上分配内存,此刻得到HeapByteBuffer
  2. 在直接内存中分配内存,此刻得到DirectByteBuffer

类图如下所示。

ByteBuffer类图

由于DirectByteBuffer是分配在直接内存中,必定无法像HeapByteBuffer相同将数据存储在字节数组,所以DirectByteBuffer会经过一个address字段来标识数据地点直接内存的开端地址。address字段界说在Buffer中,如下所示。

long address;

2. ByteBuffer的创立

ByteBuffer供给了如下四个办法用于创立ByteBuffer,如下所示。

办法 阐明
allocate(int capacity) 在堆上分配一个新的字节缓冲区。阐明如下:
1. 创立出来后,position为0,而且limit会取值为capacity
2. 创立出来的实践为HeapByteBuffer,其内部运用一个字节数组hb存储元素;
3. 初始时hb中一切元素为0
allocateDirect(int capacity) 在直接内存中分配一个新的字节缓冲区。阐明如下:
1. 创立出来后,position为0,而且limit会取值为capacity
2. 创立出来的实践为DirectByteBuffer,是根据操作系统创立的内存区域作为缓冲区;
3. 初始时一切元素为0
wrap(byte[] array) 将字节数组包装到字节缓冲区中。阐明如下:
1. 创立出来的是HeapByteBuffer,其内部的hb字节数组就会运用传入的array
2. 改动HeapByteBuffer会影响array,改动array会影响HeapByteBuffer
3. 得到的HeapByteBufferlimitcapacity均取值为array.length
4. position此刻都为0
wrap(byte[] array, int off, int length) 将字节数组包装到字节缓冲区,阐明如下。
1. 创立出来的是HeapByteBuffer,其内部的hb字节数组就会运用传入的array
2. 改动HeapByteBuffer会影响array,改动array会影响HeapByteBuffer
3. capacity取值为array.length
4. limit取值为off + length
5. position取值为off

下面结合源码,分析一下上述四种创立办法。

首先是allocate(int capacity),如下所示。

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0) {
        throw new IllegalArgumentException();
    }
    // 直接创立HeapByteBuffer
    // HeapByteBuffer(int cap, int lim)
    return new HeapByteBuffer(capacity, capacity);
}

然后是allocateDirect(int capacity),如下所示。

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {
    // MappedByteBuffer(int mark, int pos, int lim, int cap)
    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)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 经过虚引证的手法来监督DirectByteBuffer是否被废物回收
    // 然后能够及时的开释堆外内存空间
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

然后是wrap(byte[] array),如下所示。

public static ByteBuffer wrap(byte[] array) {
    return wrap(array, 0, array.length);
}

其实wrap(byte[] array) 办法便是调用的wrap(byte[] array, int off, int length),下面直接看wrap(byte[] array, int off, int length) 办法的完结。

public static ByteBuffer wrap(byte[] array, int off, int length) {
    try {
        return new HeapByteBuffer(array, off, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}

这儿先简略阐明一下上述办法中的offlength这两个参数的含义。

  1. off便是表明字节数组封装到字节缓冲区后,position的方位,所以有position = off
  2. length简略理解便是用于核算limit,即limit = position + length。其实length是理解为字节数组封装到字节缓冲区后,要运用的字节数组的长度。

下面给出一张wrap(byte[] array, int off, int length) 办法的效果示意图。

网络编程-ByteBuffer的wrap示意图

终究阐明一点,无论是wrap(byte[] array) 仍是wrap(byte[] array, int off, int length) 办法,均结构的是HeapByteBuffer

3. ByteBuffer的slice操作

ByteBuffer中界说了一个笼统办法叫做slice(),用于在已有的ByteBuffer上得到一个新的ByteBuffer,两个ByteBufferpositionlimitcapacitymark都是独立的,可是底层存储数据的内存区域是相同的,那么相应的,对其间任何一个ByteBuffer做更改,会影响到另外一个ByteBuffer

下面先看一下HeapByteBufferslice() 办法的完结。

public ByteBuffer slice() {
    return new HeapByteBuffer(hb, -1, 0, this.remaining(), this.remaining(), this.position() + offset);
}
public final int remaining() {
    return limit - position;
}
protected HeapByteBuffer(byte[] buf, int mark, int pos, int lim, int cap, int off) {
    super(mark, pos, lim, cap, buf, off);
}
ByteBuffer(int mark, int pos, int lim, int cap,
           byte[] hb, int offset) {
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

新的HeapByteBuffermark重置为了-1,position重置为了0,limit等于capacity等于老的HeapByteBuffer的未操作数据的长度(老的limit – posittion)。

此外,两个HeapByteBuffer存储数据的字节数组hb是同一个,且新的HeapByteBufferoffset等于老的HeapByteBufferposition,什么意思呢,先看下面这张图。

网络编程-HeapByteBuffer的slice示意图

意思便是,在新的HeapByteBuffer中,操作position方位的元素,实践是在操作hb[position + offset] 方位的元素,那么这儿也就解说了ByteBufferoffset特点的效果,便是表明要操作字节数组时的索引偏移量。

有了上面临HeapByteBuffer的理解,那么现在再看DirectByteBuffer就显得很简略了,DirectByteBufferslice() 办法的完结如下所示。

public ByteBuffer slice() {
    int pos = this.position();
    int lim = this.limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);
    int off = (pos << 0);
    assert (off >= 0);
    return new DirectByteBuffer(this, -1, 0, rem, rem, off);
}
DirectByteBuffer(DirectBuffer db,
                 int mark, int pos, int lim, int cap,
                 int off) {
    super(mark, pos, lim, cap);
    address = db.address() + off;
    cleaner = null;
    att = db;
}

DirectByteBufferslice() 办法的完结和HeapByteBuffer差不多,只不过在HeapByteBuffer中是对字节数组索引有偏移,而在DirectByteBuffer中是对堆外内存地址有偏移,一起偏移量都是老的ByteBufferposition的值。

终究针对slice() 办法,有一点小阐明,在DirectByteBufferatt中有这么一段注释。

If this buffer is a view of another buffer then …

这儿提到了view,翻译过来叫做视图,其实调用ByteBufferslice() 办法,能够想象成便是为原字节缓冲区创立了一个视图,这个视图和原字节缓冲区同享同一片内存区域,可是有新的一套markpositionlimitcapacity

4. ByteBuffer的asReadOnlyBuffer操作

ByteBuffer界说了一个笼统办法叫做asReadOnlyBuffer(),会在当时ByteBuffer根底上创立一个新的ByteBuffer,创立出来的ByteBuffer能看见老ByteBuffer的数据(同享同一块内存),但只能读不能写(只读的),一起两个ByteBufferpositionlimitcapacitymark是独立的。

先看一下HeapByteBufferasReadOnlyBuffer() 办法的完结,如下所示。

public ByteBuffer asReadOnlyBuffer() {
    return new HeapByteBufferR(hb,
            this.markValue(),
            this.position(),
            this.limit(),
            this.capacity(),
            offset);
}
protected HeapByteBufferR(byte[] buf,
                          int mark, int pos, int lim, int cap,
                          int off) {
    super(buf, mark, pos, lim, cap, off);
    this.isReadOnly = true;
}

也便是会new一个HeapByteBufferR出来,而且会指定其isReadOnly字段为true,表明只读。HeapByteBufferR承继于HeapByteBuffer,表明只读HeapByteBufferHeapByteBufferR重写了HeapByteBuffer的一切写相关办法,而且在这些写相关办法中抛出ReadOnlyBufferException反常,下面是部分写办法的示例。

public ByteBuffer put(int i, byte x) {
    throw new ReadOnlyBufferException();
}
public ByteBuffer put(byte x) {
    throw new ReadOnlyBufferException();
}

再看一下DirectByteBufferasReadOnlyBuffer() 办法的完结,如下所示。

public ByteBuffer asReadOnlyBuffer() {
    return new DirectByteBufferR(this,
            this.markValue(),
            this.position(),
            this.limit(),
            this.capacity(),
            0);
}
DirectByteBufferR(DirectBuffer db,
                  int mark, int pos, int lim, int cap,
                  int off) {
    super(db, mark, pos, lim, cap, off);
}

也是会new一个只读的DirectByteBufferRDirectByteBufferR承继于DirectByteBuffer偏重写了一切写相关办法,而且在这些写相关办法中抛出ReadOnlyBufferException反常。

5. ByteBuffer的写操作

ByteBuffer中界说了很多写操作相关的笼统办法,如下图所示。

ByteBuffer界说的写操作笼统办法图

总体能够进行如下归类。

ByteBuffer-写操作脑图

下面将对上述部分写办法结合源码进行阐明。

Ⅰ. put(byte)

首先是最简略的put(byte) 办法,效果是往字节缓冲区的position方位写入一个字节,先看一下HeapByteBuffer对其的完结,如下所示。

public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}
protected int ix(int i) {
    return i + offset;
}
// Buffer#nextPutIndex()
final int nextPutIndex() {
    if (position >= limit) {
        throw new BufferOverflowException();
    }
    return position++;
}

再看一下DirectByteBufferput(byte) 办法的完结,如下所示。

public ByteBuffer put(byte x) {
    unsafe.putByte(ix(nextPutIndex()), ((x)));
    return this;
}
private long ix(int i) {
    return address + ((long)i << 0);
}
// Buffer#nextPutIndex()
final int nextPutIndex() {
    if (position >= limit) {
        throw new BufferOverflowException();
    }
    return position++;
}

都是会调用到Buffer#nextPutIndex() 办法来拿到当时的position,区别是HeapByteBuffer是将字节写入到堆上的数组,而DirectByteBuffer是写在直接内存中。

Ⅱ. put(int, byte)

put(int, byte) 办法能够在指定方位写入一个字节,注意该办法写入字节不会改动position

HeapByteBuffer对其完结如下所示。

public ByteBuffer put(int i, byte x) {
    hb[ix(checkIndex(i))] = x;
    return this;
}
protected int ix(int i) {
    return i + offset;
}
// Buffer#checkIndex(int)
final int checkIndex(int i) {
    if ((i < 0) || (i >= limit)) {
        throw new IndexOutOfBoundsException();
    }
    return i;
}

DirectByteBufferput(int, byte) 办法的完结如下所示。

public ByteBuffer put(int i, byte x) {
    unsafe.putByte(ix(checkIndex(i)), ((x)));
    return this;
}
private long ix(int i) {
    return address + ((long) i << 0);
}
// Buffer#checkIndex(int)
final int checkIndex(int i) {
    if ((i < 0) || (i >= limit)) {
        throw new IndexOutOfBoundsException();
    }
    return i;
}
Ⅲ. put(byte[], int, int)

put(byte[], int, int) 办法是批量的将字节数组中指定的字节写到ByteBuffer

put(byte[], int, int) 办法并不是笼统办法,在ByteBuffer中界说了其完结,但一起HeapByteBufferDirectByteBuffer也都对其进行了重写。下面别离看一下其完结。

ByteBuffer#put(byte[], int, int) 完结如下所示。

public ByteBuffer put(byte[] src, int offset, int length) {
    checkBounds(offset, length, src.length);
    if (length > remaining()) {
        throw new BufferOverflowException();
    }
    int end = offset + length;
    // 从src的offset索引开端顺次将后续的length个字节写到ByteBuffer中
    for (int i = offset; i < end; i++) {
        this.put(src[i]);
    }
    return this;
}

ByteBufferput(byte[], int, int) 办法的完结是循环遍历字节数组中每一个需求写入的字节,然后调用put(byte) 办法完结写入,其间offset表明从字节数组的哪一个字节开端写,length表明从offset开端往后的多少个字节需求写入。

由于ByteBufferput(byte[], int, int) 办法的完结的写入功率不高,所以HeapByteBufferDirectByteBuffer都有自己的完结,先看一下HeapByteBufferput(byte[], int, int) 办法的完结,如下所示。

public ByteBuffer put(byte[] src, int offset, int length) {
    checkBounds(offset, length, src.length);
    if (length > remaining()) {
        throw new BufferOverflowException();
    }
    // 运用了native的复制办法来完结更高效的写入
    System.arraycopy(src, offset, hb, ix(position()), length);
    position(position() + length);
    return this;
}

由于HeapByteBuffer存储字节是存储到字节数组中,所以直接运用nativearraycopy() 办法来完结字节数组的复制是更为高效的手法。

再看一下DirectByteBufferput(byte[], int, int) 办法的完结,如下所示。

public ByteBuffer put(byte[] src, int offset, int length) {
    // 写入字节数大于6时运用native办法来批量写入才更高效
    if (((long) length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
        checkBounds(offset, length, src.length);
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        if (length > rem) {
            throw new BufferOverflowException();
        }
        // 这儿终究会调用到native办法Unsafe#copyMemory来批量写入
        Bits.copyFromArray(src, arrayBaseOffset,
                (long) offset << 0,
                ix(pos),
                (long) length << 0);
        // 更新position
        position(pos + length);
    } else {
        // 写入字节数小于等于6则遍历每个字节并顺次写入会更高效
        super.put(src, offset, length);
    }
    return this;
}

DirectByteBuffer的完结中,并没有直接调用到native办法来批量操作直接内存,而是先做了判别:假如本次批量写入的字节数大于JNI_COPY_FROM_ARRAY_THRESHOLD(默许是6),才调用native办法Unsafe#copyMemory来完结字节在直接内存中的批量写入,否则就仍是一个字节一个字节的写入。DirectByteBuffer的做法主要仍是考虑到native办法的调用的一个开支,比方就写入一个字节,那必定是没有必要调用native办法的。

Ⅳ. put(byte[])

put(byte[]) 办法的效果是将一个字节数组的内容悉数写入到ByteBuffer,该办法是一个final办法,所以这儿看一下ByteBuffer中该办法的完结,如下所示。

public final ByteBuffer put(byte[] src) {
    return put(src, 0, src.length);
}

其实便是调用到put(byte[], int, int) 办法来完结批量写入。

Ⅴ. put(ByteBuffer)

put(ByteBuffer) 办法用于将一个ByteBuffer中一切未操作的字节批量写入当时ByteBufferByteBufferHeapByteBufferDirectByteBuffer都有相应的完结,下面别离看一下。

ByteBuffer#put(ByteBuffer) 思路仍是一个字节一个字节的写入,完结如下。

public ByteBuffer put(ByteBuffer src) {
    if (src == this) {
        throw new IllegalArgumentException();
    }
    if (isReadOnly()) {
        throw new ReadOnlyBufferException();
    }
    // 核算limit - position
    int n = src.remaining();
    if (n > remaining()) {
        throw new BufferOverflowException();
    }
    // 一个字节一个字节的写入
    for (int i = 0; i < n; i++) {
        put(src.get());
    }
    return this;
}

HeapByteBuffer#put(ByteBuffer) 思路是先判别源ByteBuffer的类型,假如源ByteBufferHeapByteBuffer,则调用native办法System#arraycopy完结批量写入,假如源ByteBuffer是在直接内存中分配的,则再判别一下要写入的字节是否大于6,假如大于6就调用native办法Unsafe#copyMemory完结批量写入,否则就一个字节一个字节的写入。完结如下。

public ByteBuffer put(ByteBuffer src) {
    if (src instanceof HeapByteBuffer) {
        if (src == this) {
            throw new IllegalArgumentException();
        }
        HeapByteBuffer sb = (HeapByteBuffer) src;
        // 核算源ByteBuffer剩下的字节数
        int n = sb.remaining();
        if (n > remaining()) {
            throw new BufferOverflowException();
        }
        // 调用native办法批量写入
        System.arraycopy(sb.hb, sb.ix(sb.position()),
                hb, ix(position()), n);
        // 更新源ByteBuffer的position
        sb.position(sb.position() + n);
        // 更新当时ByteBuffer的position
        position(position() + n);
    } else if (src.isDirect()) {
        // 核算源ByteBuffer剩下的字节数
        int n = src.remaining();
        if (n > remaining()) {
            throw new BufferOverflowException();
        }
        // 批量写入字节到当时ByteBuffer的hb字节数组中
        src.get(hb, ix(position()), n);
        // 更新当时ByteBuffer的position
        position(position() + n);
    } else {
        super.put(src);
    }
    return this;
}
// DirectByteBuffer#get(byte[], int, int)
public ByteBuffer get(byte[] dst, int offset, int length) {
    if (((long) length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
        checkBounds(offset, length, dst.length);
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        if (length > rem) {
            throw new BufferUnderflowException();
        }
        Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
                (long) offset << 0,
                (long) length << 0);
        // 更新源ByteBuffer的position
        position(pos + length);
    } else {
        super.get(dst, offset, length);
    }
    return this;
}

DirectByteBuffer#put(ByteBuffer) 的思路也是先判别源ByteBuffer的类型,假如源ByteBufferDirectByteBuffer,则直接运用native办法Unsafe#copyMemory完结批量写入,假如源ByteBuffer是在堆上分配的,则依照DirectByteBufferput(byte[], int, int) 办法的逻辑完结批量写入。完结如下所示。

public ByteBuffer put(ByteBuffer src) {
    if (src instanceof DirectByteBuffer) {
        if (src == this) {
            throw new IllegalArgumentException();
        }
        DirectByteBuffer sb = (DirectByteBuffer) src;
        int spos = sb.position();
        int slim = sb.limit();
        assert (spos <= slim);
        // 核算源ByteBuffer剩下的字节数
        int srem = (spos <= slim ? slim - spos : 0);
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        if (srem > rem) {
            throw new BufferOverflowException();
        }
        // 调用native办法完结批量写入
        unsafe.copyMemory(sb.ix(spos), ix(pos), (long) srem << 0);
        // 更新源ByteBuffer的position
        sb.position(spos + srem);
        // 更新当时ByteBuffer的position
        position(pos + srem);
    } else if (src.hb != null) {
        int spos = src.position();
        int slim = src.limit();
        assert (spos <= slim);
        // 核算源ByteBuffer剩下的字节数
        int srem = (spos <= slim ? slim - spos : 0);
        // 调用DirectByteBuffer#put(byte[], int, int)完结批量写入
        put(src.hb, src.offset + spos, srem);
        // 更新源ByteBuffer的position
        src.position(spos + srem);
    } else {
        super.put(src);
    }
    return this;
}
// DirectByteBuffer#put(byte[], int, int)
public ByteBuffer put(byte[] src, int offset, int length) {
    if (((long) length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
        checkBounds(offset, length, src.length);
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        if (length > rem) {
            throw new BufferOverflowException();
        }
        Bits.copyFromArray(src, arrayBaseOffset,
                (long) offset << 0,
                ix(pos),
                (long) length << 0);
        // 更新当时ByteBuffer的position
        position(pos + length);
    } else {
        super.put(src, offset, length);
    }
    return this;
}

终究有一点需求阐明,调用put(ByteBuffer) 办法完结批量字节写入后,源ByteBuffer和当时ByteBufferposition都会被更新。

Ⅵ. 字节序

上述的几种put() 办法都是向ByteBuffer写入字节,但其实也是能够直接将charint等根底数据类型写入ByteBuffer,但在分析这些写入根底数据类型到ByteBufferput() 办法以前,有必要对字节序的相关概念进行演示和阐明。

已知在Java中一个int是四个字节,而一个字节是8位,那么就以数字23333为例,示意如下。

网络编程-int字节表明图

那么上述的一个int数据,存储在内存中时,假如高位字节存储在内存的低地址,低位字节存储在内存的高地址,这种就称为大端字节序(Big Endian),示意图如下所示。

网络编程-大端字节序示意图

反之假如低位字节存储在内存的低地址,高位字节存储在内存的高地址,这种就称为小端字节序(Little Endian),示意图如下所示。

网络编程-小端字节序示意图

上述其实是主机字节序,表明核算机内存中字节的存储次序。在Java中,数据的存储默许是依照大端字节序来存储的。

然后还有一种叫做网络字节序,表明网络传输中字节的传输次序,分类如下。

  1. 大端字节序(Big Endian)。从二进制数据的高位开端传输;
  2. 小端字节序(Little Endian)。从二进制数据的低位开端传输。

在网络传输中,默许依照大端字节序来传输。

Ⅶ. putInt(int)

putInt(int) 办法是ByteBuffer界说的用于直接写入一个int的笼统办法,先看HeapByteBuffer的完结,如下所示。

public ByteBuffer putInt(int x) {
    // 经过nextPutIndex(4)办法拿到当时position,并让position加4
    // 然后调用Bits#putInt完结写入,其间bigEndian默许是true
    Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);
    return this;
}
// Bits#putInt
static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {
    if (bigEndian) {
        putIntB(bb, bi, x);
    } else {
        putIntL(bb, bi, x);
    }
}
// Bits#putIntB
static void putIntB(ByteBuffer bb, int bi, int x) {
    // 经过Bits#int3办法拿到x的第3字节(最高位字节)
    // 然后写入到hb字节数组的索引为bi的方位
    bb._put(bi    , int3(x));
    // 经过Bits#int2办法拿到x的第2字节(次高位字节)
    // 然后写入到hb字节数组的索引为bi+1的方位
    bb._put(bi + 1, int2(x));
    // 经过Bits#int1办法拿到x的第1字节(次低位字节)
    // 然后写入到hb字节数组的索引为bi+2的方位
    bb._put(bi + 2, int1(x));
    // 经过Bits#int0办法拿到x的第0字节(最低位字节)
    // 然后写入到hb字节数组的索引为bi+3的方位
    bb._put(bi + 3, int0(x));
}
// Bits#int3
private static byte int3(int x) {
    return (byte) (x >> 24);
}
// HeapByteBuffer#_put
void _put(int i, byte b) {
    hb[i] = b;
}

HeapByteBuffer完结的putInt(int) 办法中,会顺次将int的高位到低位写入到hb字节数组的低索引到高索引,而在堆中,内存地址是由低到高的,也便是跟着数组索引的添加,内存地址也会逐渐增高,所以上述的便是依照大端字节序的办法来直接写入一个int

再看一下DirectByteBufferputInt(int) 办法的完结,如下所示。

public ByteBuffer putInt(int x) {
    // 经过nextPutIndex(4)办法拿到当时position,并让position加4
    // 经过ix()办法拿到实践要写入的内存地址
    putInt(ix(nextPutIndex((1 << 2))), x);
    return this;
}
// DirectByteBuffer#putInt(long, int)
private ByteBuffer putInt(long a, int x) {
    if (unaligned) {
        int y = (x);
        unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
    } else {
        // 调用Bits#putInt完结写入,其间bigEndian默许是true
        Bits.putInt(a, x, bigEndian);
    }
    return this;
}
// Bits#putInt
static void putInt(long a, int x, boolean bigEndian) {
    if (bigEndian) {
        putIntB(a, x);
    } else {
        putIntL(a, x);
    }
}
// Bits#putIntB
static void putIntB(long a, int x) {
    // 经过Bits#int3办法拿到x的第3字节(最高位字节)
    // 然后写入到直接内存地址为a的方位
    _put(a    , int3(x));
    // 经过Bits#int2办法拿到x的第2字节(次高位字节)
    // 然后写入到直接内存地址为a+1的方位
    _put(a + 1, int2(x));
    // 经过Bits#int1办法拿到x的第1字节(次低位字节)
    // 然后写入到直接内存地址为a+2的方位
    _put(a + 2, int1(x));
    // 经过Bits#int0办法拿到x的第0字节(最低位字节)
    // 然后写入到直接内存地址为a+3的方位
    _put(a + 3, int0(x));
}
// Bits#int3
private static byte int3(int x) {
    return (byte) (x >> 24);
}
// Bits#_put
private static void _put(long a, byte b) {
    unsafe.putByte(a, b);
}

DirectByteBuffer的完结中,会顺次将int的高位到低位写入到直接内存的低地址到高地址,全体也是一个大端字节序的写入办法。

Ⅷ. putInt(int, int)

putInt(int, int) 办法能够在指定方位写入int,一起也不会更改positionputInt(int, int) 办法完结原理和putInt(int) 相同,故这儿不再赘述。

其它的写入非字节的办法,实质和写入int共同,故也不再赘述。

6. ByteBuffer的读操作

ByteBuffer中界说了很多读操作相关的笼统办法,如下图所示。

ByteBuffer界说的读操作笼统办法图

总体能够进行如下归类。

ByteBuffer-读操作脑图

下面将对上述部分读办法结合源码进行阐明。

Ⅰ. get()

get() 办法用于读取一个字节,HeapByteBuffer的完结如下所示。

public byte get() {
    return hb[ix(nextGetIndex())];
}

上述办法是读取字节数组中position索引方位的字节,然后position加1。再看一下DirectByteBufferget() 办法的完结,如下所示。

public byte get() {
    return ((unsafe.getByte(ix(nextGetIndex()))));
}

上述办法是根据native办法拿到address + position方位的字节然后position加1。

Ⅱ. get(int)

get(int) 办法用于读取指定方位的字节,HeapByteBuffer的完结如下所示。

public byte get(int i) {
    return hb[ix(checkIndex(i))];
}

上述办法会读取字节数组中指定索引方位的字节,注意position不会改动。再看一下DirectByteBufferget(int) 办法的完结,如下所示。

public byte get(int i) {
    return ((unsafe.getByte(ix(checkIndex(i)))));
}

上述办法是根据native办法拿到指定方位的字节,相同,position不会改动。

Ⅲ. get(byte[], int, int)

get(byte[], int, int) 办法用于将当时ByteBufferposition方位开端往后的若干字节写入到方针字节数组的指定方位。ByteBufferHeapByteBufferDirectByteBuffer都有相应的完结,下面别离看一下。

ByteBufferget(byte[], int, int) 办法的完结中是一个字节一个字节的读取并写入,如下所示。

public ByteBuffer get(byte[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining()) {
        throw new BufferUnderflowException();
    }
    int end = offset + length;
    // 写入方针数组的开端方位是offset
    // 共写入length个字节
    for (int i = offset; i < end; i++) {
        dst[i] = get();
    }
    return this;
}

HeapByteBufferget(byte[], int, int) 办法的完结中,是调用System#arraycopy本地办法来进行批量复制写入,功率比一个字节一个字节的读取并写入更高,且终究会更新当时HeapByteBufferposition

public ByteBuffer get(byte[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining()) {
        throw new BufferUnderflowException();
    }
    // 调用native办法来批量写入字节到dst字节数组
    System.arraycopy(hb, ix(position()), dst, offset, length);
    // 更新当时HeapByteBuffer的position
    position(position() + length);
    return this;
}

DirectByteBufferget(byte[], int, int) 办法的完结中,会先判别需求读取并写入到方针字节数组中的字节数是否大于6,大于6时会调用native办法来批量写入,否则就一个字节一个字节的读取并写入,终究还会更新当时DirectByteBufferposition

public ByteBuffer get(byte[] dst, int offset, int length) {
    if (((long) length << 0) > Bits.JNI_COPY_TO_ARRAY_THRESHOLD) {
        // 批量写入的字节数大于6个
        checkBounds(offset, length, dst.length);
        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        if (length > rem) {
            throw new BufferUnderflowException();
        }
        // 终究调用到Unsafe#copyMemory办法完结批量复制写入
        Bits.copyToArray(ix(pos), dst, arrayBaseOffset,
                (long) offset << 0,
                (long) length << 0);
        // 更新当时DirectByteBuffer的position
        position(pos + length);
    } else {
        // 批量写入的字节数小于等于6个
        // 则一个字节一个字节的读取并写入
        super.get(dst, offset, length);
    }
    return this;
}
Ⅳ. get(byte[])

get(byte[]) 办法会从当时ByteBufferposition方位开端,读取方针字节数组长度个字节,然后顺次写入到方针字节数组。get(byte[]) 办法由ByteBuffer完结,如下所示。

public ByteBuffer get(byte[] dst) {
    return get(dst, 0, dst.length);
}

那么实质仍是依赖get(byte[], int, int) 办法,只不过将offset指定为了0(表明从dst字节数组的索引为0的方位开端写入),将length指定为了dst.length(表明要写满dst字节数组)。

Ⅴ. getInt()

getInt() 办法表明从ByteBuffer中读取一个int值,先看一下HeapByteBuffer的完结,如下所示。

public int getInt() {
    // 经过nextGetIndex(4)拿到当时position,然后position加4
    // 默许bigEndian为true,表明以大端字节序的办法读取int
    return Bits.getInt(this, ix(nextGetIndex(4)), bigEndian);
}
// Bits#getInt
static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
    return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
}
// Bits#getIntB
static int getIntB(ByteBuffer bb, int bi) {
    // 顺次拿到低索引到高索引的字节
    // 这些字节顺次对应int值的高位到低位
    // 终究调用makeInt()办法拼接成int值
    return makeInt(bb._get(bi),
            bb._get(bi + 1),
            bb._get(bi + 2),
            bb._get(bi + 3));
}
// Bits#makeInt
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
    return (((b3) << 24) |
            ((b2 & 0xff) << 16) |
            ((b1 & 0xff) << 8) |
            ((b0 & 0xff)));
}
// HeapByteBuffer#_get
byte _get(int i) {
    return hb[i];
}

上述办法的完结中,首先获取到当时HeapByteBufferposition,然后从position方位开端顺次读取四个字节,由于默许状况下是大端字节序(也便是写和读都是依照大端字节序的办法),所以读取到的字节应该顺次对应int值的高位到低位,所以终究会在Bits#makeInt办法中将四个字节经过错位或的办法得到int值。

再看一下DirectByteBuffergetInt() 办法的完结,如下所示。

public int getInt() {
    // 经过nextGetIndex(4)拿到当时position,然后position加4
    // 经过ix()办法拿到操作的开端地址是address + position
    return getInt(ix(nextGetIndex((1 << 2))));
}
// DirectByteBuffer#getInt(long)
private int getInt(long a) {
    if (unaligned) {
        int x = unsafe.getInt(a);
        return (nativeByteOrder ? x : Bits.swap(x));
    }
    // 默许bigEndian为true,表明默许大端字节序
    return Bits.getInt(a, bigEndian);
}
// Bits#getInt
static int getInt(long a, boolean bigEndian) {
    return bigEndian ? getIntB(a) : getIntL(a) ;
}
// Bits#getIntB
static int getIntB(long a) {
    // 从低地址拿到int值的高位
    // 从高地址拿到int值的低位
    // 然后拼接得到终究的int值
    return makeInt(_get(a),
            _get(a + 1),
            _get(a + 2),
            _get(a + 3));
}
// Bits#_get
private static byte _get(long a) {
    return unsafe.getByte(a);
}
// Bits#makeInt
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
    return (((b3) << 24) |
            ((b2 & 0xff) << 16) |
            ((b1 & 0xff) << 8) |
            ((b0 & 0xff)));
}

DirectByteBuffergetInt() 办法的全体完结思路和HeapByteBuffer是共同的,在默许大端字节序的状况下,从低地址拿到int值的高位字节,从高地址拿到int值的低位字节,终究经过错位或的办法得到终究的int值。请注意,操作完结后,position都会加4,这是由于一个int占四个字节,也便是相当于读取了4个字节。

Ⅵ. getInt(int)

getInt(int) 办法能够从指定方位读取一个int值,完结思路和getInt() 办法完全共同,故这儿不再赘述,但需求注意的是,getInt(int) 办法读取一个int值后,不会改动position

其它的非字节的读取,实质和int值的读取相同,故也不再赘述。

7. ByteBuffer的运用示例

Log4j2日志框架中,终究在将日志进行输出时,对日志内容的处理就有运用到ByteBuffer,下面一起来简略的看一下。(无需重视Log4j2的完结细节)

在将日志内容进行规范输出时,终究是经过OutputStreamManager完结将日志内容输出,它里边有一个字段便是HeapByteBuffer,用于存储日志内容的字节数据。下面先看一下org.apache.logging.log4j.core.layout.TextEncoderHelper#writeEncodedText办法,这儿面会有OutputStreamManagerByteBuffer怎么被写入的相关逻辑。

private static void writeEncodedText(final CharsetEncoder charsetEncoder, final CharBuffer charBuf,
        final ByteBuffer byteBuf, final ByteBufferDestination destination, CoderResult result) {
    ......
    if (byteBuf != destination.getByteBuffer()) {
        // 这儿的byteBuf存储了处理后的日志内容
        // 调用flip()办法来进入读形式
        byteBuf.flip();
        // 这儿的destination便是OutputStreamManager
        // 这儿会将byteBuf的内容写到OutputStreamManager的ByteBuffer中
        destination.writeBytes(byteBuf);
        // 切换为写形式,也便是position置0,重置limit等于capacity等
        byteBuf.clear();
    }
}
// OutputStreamManager#writeBytes
public void writeBytes(final ByteBuffer data) {
    if (data.remaining() == 0) {
        return;
    }
    synchronized (this) {
        ByteBufferDestinationHelper.writeToUnsynchronized(data, this);
    }
}
// ByteBufferDestinationHelper#writeToUnsynchronized
public static void writeToUnsynchronized(final ByteBuffer source, final ByteBufferDestination destination) {
    // 拿到OutputStreamManager中的HeapByteBuffer
    // 这儿称OutputStreamManager中的HeapByteBuffer为方针ByteBuffer
    ByteBuffer destBuff = destination.getByteBuffer();
    // 假如源ByteBuffer剩下可读字节多于方针ByteBuffer剩下可写字节
    // 则循环的写满方针ByteBuffer再读取完方针ByteBuffer
    // 终究便是需求将源ByteBuffer的字节悉数由方针ByteBuffer消费掉
    while (source.remaining() > destBuff.remaining()) {
        final int originalLimit = source.limit();
        // 先将源ByteBuffer的limit设置为当时position + 方针ByetBuffer剩下可写字节数
        source.limit(Math.min(source.limit(), source.position() + destBuff.remaining()));
        // 将源ByteBuffer当时position到limit的字节写到方针ByteBuffer中
        destBuff.put(source);
        // 康复源ByteBuffer的limit
        source.limit(originalLimit);
        // 方针ByteBuffer先将已有的字节悉数规范输出
        // 然后回来一个写形式的方针ByteBuffer
        destBuff = destination.drain(destBuff);
    }
    // 到这儿阐明源ByteBuffer剩下可读字节小于等于方针ByteBuffer剩下可写字节
    // 则将源ByteBuffer剩下可读字节悉数写到方针ByteBuffer中
    // 后续会在其它地方将这部分内容悉数规范输出
    destBuff.put(source);
}
// OutputStreamManager#drain
public ByteBuffer drain(final ByteBuffer buf) {
    flushBuffer(buf);
    return buf;
}
// OutputStreamManager#flushBuffer
protected synchronized void flushBuffer(final ByteBuffer buf) {
    // 方针ByteBuffer切换为读形式
    ((Buffer) buf).flip();
    try {
        if (buf.remaining() > 0) {
            // 拿到HeapByteBuffer中的字节数组
            // 终究调用到PrintStream来规范输出字节数组中的字节内容
            writeToDestination(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
        }
    } finally {
        // 方针ByteBuffer切换回写形式
        buf.clear();
    }
}

信任假如阅读完本文,那么上述Log4j2中关于ByteBuffer的运用,必定都是能看理解的,尽管Log4j2中有很多的根据ByteBuffer的运用,可是终究的规范输出仍是根据Java的传统IO来输出的,那么为什么中间还要用ByteBuffer来多处理一下呢,其实也便是由于ByteBuffer在读写字节时会考虑功能问题,会运用到功能更高的native办法来批量的操作字节数据,因此以快著称的Log4j2挑选了NIO中的ByteBuffer

8. ByteBuffer的缺陷

假如要评论ByteBuffer的缺陷,其实能够结合第7末节的运用示例来一并评论。

首先便是读写形式的切换。在第7末节示例中,会发现存在多处调用flip() 办法来切换到读形式,调用clear() 办法来切换到写形式,这种形式的切换,既麻烦,还容易出错。

然后便是无法扩容。在第7末节示例中,有一个细节便是由于ByteBuffer容量太小了,无法一次写完一切字节数据,所以就只能循环的写满读取然后再写满这样子来操作,假如能扩容就不用这么麻烦了。

终究便是线程不安全ByteBuffer自身并没有供给对线程安全的保护,要完结线程安全,需求运用者自己经过其它的并发语义来完结。

总结

本文对ByteBuffer的分析能够参照下图。

ByteBuffer-知识点脑图

为啥NIO中偏分析ByteBuffer呢,由于Netty中的缓存ByteBuf,其对ByteBuffer做了改良,鄙人一篇文章中,将对Netty中的缓存ByteBuf进行具体分析。

假如觉得本篇文章对你有协助,求求你点个赞,加个收藏终究再点个重视吧。创作不易,感谢支撑!


本文正在参加「金石计划」