学妹觉得我之前写的Reactor模型还不错,
问我是不是能够再总结一下ByteBuffer,
其实平时不怎么会运用ByteBuffer的,
可是架不住学妹一杯奶茶,
那就简略的总结一下吧。
前言
已知NIO中有三大组件:Channel,Buffer和Selector。那么Buffer的效果便是供给一个缓冲区,用于用户程序和Channel之间进行数据读写,也便是用户程序中能够运用Buffer向Channel写入数据,也能够运用Buffer从Channel读取数据。
ByteBuffer是Buffer子类,是字节缓冲区,特色如下所示。
- 巨细不行变。一旦创立,无法改动其容量巨细,无法扩容或许缩容;
- 读写灵活。内部经过指针移动来完结灵活读写;
- 支撑堆上内存分配和直接内存分配。
本文将对ByteBuffer的相关概念,常用API以及运用事例进行分析。全文约1万字,知识点脑图如下。
正文
一. Buffer
在NIO中,八大根底数据类型中除了boolean外,都有相应的Buffer的完结,类图如下所示。
Buffer类对各种根底数据类型的缓冲区做了顶层笼统,所以要了解ByteBuffer,首先应该学习Buffer类。
1. Buffer的特点
一切缓冲区结构都有如下特点。
特点 | 阐明 |
---|---|
int position | 方位索引。代表下一次即将操作的元素的方位,默许初始为0,方位索引最小为0,最大为limit |
int limit | 约束索引。约束索引及之后的索引方位上的元素都不能操作,约束索引最小为0,最大为capacity |
int capacity | 容量。缓冲区的最大元素个数,创立缓冲区时指定,最小为0,不能改动 |
三者之间的巨细联系应该是:0 <= position <= limit <= capacity,图示如下。
除此之外,还有一个特点叫做mark,如下所示。
特点 | 阐明 |
---|---|
int mark | 符号索引。mark会符号一个索引,在Buffer#reset调用时,将position重置为mark。mark不是有必要的,可是当界说mark后,其最小为0,最大为position |
关于mark还有如下两点阐明。
- position或limit一旦小于mark则mark会被丢弃;
- 没有界说mark时假如调用了Buffer#reset则会抛出InvalidMarkException。
2. Buffer的读形式
Buffer有两种形式,读形式和写形式,在读形式下,能够读取缓冲区中的数据。那么关于一个缓冲区,要读取数据时,分为两步。
- 拿到position方位索引;
- 取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的写形式下,写入数据也是分为两步。
- 拿到position方位索引;
- 写入数据到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供给了对应的办法来做读写形式切换。
首先是读形式切换到写形式,先看如下示意图。
上图中的状况是缓冲区中的数据现已悉数被读完,那么此刻假如要切换到写形式,对应的办法是clear() 办法,如下所示。
public final Buffer clear() {
// 重置position为0
position = 0;
// 设置limit为capacity
limit = capacity;
// 重置mark为-1
mark = -1;
return this;
}
注意,尽管办法名叫做clear(),可是实践缓冲区中的数据并没有被铲除,而只是将方位索引position,约束索引limit进行了重置,一起铲除了符号状态(也便是将mark设置为-1)。切换到写形式后,缓冲区示意图如下所示。
然后是写形式切换到读形式,先看如下示意图。
数据现已写入结束了,此刻假如要切换到读形式,对应的办法是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的上一个方位,然后重置position和mark。切换到读形式后,缓冲区示意图如下所示。
5. Buffer的rewind操作
在运用Buffer时,能够针对现已操作的区域进行重操作,假定缓冲区示意图如下。
再看一下rewind() 办法的完结,如下所示。
public final Buffer rewind() {
// 重置position为0
position = 0;
// 铲除mark
mark = -1;
return this;
}
主要便是将方位索引position重置为0,这样就能从头操作现已操作过的方位了,一起假如启用了mark,那么还会铲除mark,也便是重置mark为-1。rewind() 办法调用后的缓冲区示意图如下所示。
6. Buffer的reset操作
在运用Buffer时,能够启用mark来符号一个现已操作过的方位,假定缓冲区示意图如下。
再看一下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() 办法调用后的缓冲区示意图如下所示。
二. ByteBuffer
在上一节主要对Buffer进行了一个阐明,那么本节会在上一节的根底上,对ByteBuffer及其完结进行学习。
1. ByteBuffer的特点
ByteBuffer相较于Buffer,多了如下三个特点。
特点 | 阐明 |
---|---|
byte[] hb | 字节数组。仅HeapByteBuffer会运用到,HeapByteBuffer的数据存储在hb中 |
int offset | 偏移量。仅HeapByteBuffer会运用到,后面会具体阐明 |
isReadOnly | 是否只读。仅HeapByteBuffer会运用到,后面会具体阐明 |
NIO中为ByteBuffer分配内存时,能够有两种办法。
- 在堆上分配内存,此刻得到HeapByteBuffer;
- 在直接内存中分配内存,此刻得到DirectByteBuffer。
类图如下所示。
由于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. 得到的HeapByteBuffer的limit和capacity均取值为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();
}
}
这儿先简略阐明一下上述办法中的off和length这两个参数的含义。
- off便是表明字节数组封装到字节缓冲区后,position的方位,所以有position = off;
- length简略理解便是用于核算limit,即limit = position + length。其实length是理解为字节数组封装到字节缓冲区后,要运用的字节数组的长度。
下面给出一张wrap(byte[] array, int off, int length) 办法的效果示意图。
终究阐明一点,无论是wrap(byte[] array) 仍是wrap(byte[] array, int off, int length) 办法,均结构的是HeapByteBuffer。
3. ByteBuffer的slice操作
在ByteBuffer中界说了一个笼统办法叫做slice(),用于在已有的ByteBuffer上得到一个新的ByteBuffer,两个ByteBuffer的position,limit,capacity和mark都是独立的,可是底层存储数据的内存区域是相同的,那么相应的,对其间任何一个ByteBuffer做更改,会影响到另外一个ByteBuffer。
下面先看一下HeapByteBuffer对slice() 办法的完结。
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;
}
新的HeapByteBuffer的mark重置为了-1,position重置为了0,limit等于capacity等于老的HeapByteBuffer的未操作数据的长度(老的limit – posittion)。
此外,两个HeapByteBuffer存储数据的字节数组hb是同一个,且新的HeapByteBuffer的offset等于老的HeapByteBuffer的position,什么意思呢,先看下面这张图。
意思便是,在新的HeapByteBuffer中,操作position方位的元素,实践是在操作hb[position + offset] 方位的元素,那么这儿也就解说了ByteBuffer中offset特点的效果,便是表明要操作字节数组时的索引偏移量。
有了上面临HeapByteBuffer的理解,那么现在再看DirectByteBuffer就显得很简略了,DirectByteBuffer对slice() 办法的完结如下所示。
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;
}
DirectByteBuffer对slice() 办法的完结和HeapByteBuffer差不多,只不过在HeapByteBuffer中是对字节数组索引有偏移,而在DirectByteBuffer中是对堆外内存地址有偏移,一起偏移量都是老的ByteBuffer的position的值。
终究针对slice() 办法,有一点小阐明,在DirectByteBuffer的att中有这么一段注释。
If this buffer is a view of another buffer then …
这儿提到了view,翻译过来叫做视图,其实调用ByteBuffer的slice() 办法,能够想象成便是为原字节缓冲区创立了一个视图,这个视图和原字节缓冲区同享同一片内存区域,可是有新的一套mark,position,limit和capacity。
4. ByteBuffer的asReadOnlyBuffer操作
ByteBuffer界说了一个笼统办法叫做asReadOnlyBuffer(),会在当时ByteBuffer根底上创立一个新的ByteBuffer,创立出来的ByteBuffer能看见老ByteBuffer的数据(同享同一块内存),但只能读不能写(只读的),一起两个ByteBuffer的position,limit,capacity和mark是独立的。
先看一下HeapByteBuffer对asReadOnlyBuffer() 办法的完结,如下所示。
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,表明只读HeapByteBuffer,HeapByteBufferR重写了HeapByteBuffer的一切写相关办法,而且在这些写相关办法中抛出ReadOnlyBufferException反常,下面是部分写办法的示例。
public ByteBuffer put(int i, byte x) {
throw new ReadOnlyBufferException();
}
public ByteBuffer put(byte x) {
throw new ReadOnlyBufferException();
}
再看一下DirectByteBuffer对asReadOnlyBuffer() 办法的完结,如下所示。
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一个只读的DirectByteBufferR,DirectByteBufferR承继于DirectByteBuffer偏重写了一切写相关办法,而且在这些写相关办法中抛出ReadOnlyBufferException反常。
5. 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++;
}
再看一下DirectByteBuffer对put(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;
}
DirectByteBuffer对put(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中界说了其完结,但一起HeapByteBuffer和DirectByteBuffer也都对其进行了重写。下面别离看一下其完结。
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;
}
ByteBuffer对put(byte[], int, int) 办法的完结是循环遍历字节数组中每一个需求写入的字节,然后调用put(byte) 办法完结写入,其间offset表明从字节数组的哪一个字节开端写,length表明从offset开端往后的多少个字节需求写入。
由于ByteBuffer对put(byte[], int, int) 办法的完结的写入功率不高,所以HeapByteBuffer和DirectByteBuffer都有自己的完结,先看一下HeapByteBuffer对put(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存储字节是存储到字节数组中,所以直接运用native的arraycopy() 办法来完结字节数组的复制是更为高效的手法。
再看一下DirectByteBuffer对put(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中一切未操作的字节批量写入当时ByteBuffer。ByteBuffer,HeapByteBuffer和DirectByteBuffer都有相应的完结,下面别离看一下。
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的类型,假如源ByteBuffer是HeapByteBuffer,则调用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的类型,假如源ByteBuffer是DirectByteBuffer,则直接运用native办法Unsafe#copyMemory完结批量写入,假如源ByteBuffer是在堆上分配的,则依照DirectByteBuffer的put(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和当时ByteBuffer的position都会被更新。
Ⅵ. 字节序
上述的几种put() 办法都是向ByteBuffer写入字节,但其实也是能够直接将char,int等根底数据类型写入ByteBuffer,但在分析这些写入根底数据类型到ByteBuffer的put() 办法以前,有必要对字节序的相关概念进行演示和阐明。
已知在Java中一个int是四个字节,而一个字节是8位,那么就以数字23333为例,示意如下。
那么上述的一个int数据,存储在内存中时,假如高位字节存储在内存的低地址,低位字节存储在内存的高地址,这种就称为大端字节序(Big Endian),示意图如下所示。
反之假如低位字节存储在内存的低地址,高位字节存储在内存的高地址,这种就称为小端字节序(Little Endian),示意图如下所示。
上述其实是主机字节序,表明核算机内存中字节的存储次序。在Java中,数据的存储默许是依照大端字节序来存储的。
然后还有一种叫做网络字节序,表明网络传输中字节的传输次序,分类如下。
- 大端字节序(Big Endian)。从二进制数据的高位开端传输;
- 小端字节序(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。
再看一下DirectByteBuffer对putInt(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,一起也不会更改position。putInt(int, int) 办法完结原理和putInt(int) 相同,故这儿不再赘述。
其它的写入非字节的办法,实质和写入int共同,故也不再赘述。
6. ByteBuffer的读操作
ByteBuffer中界说了很多读操作相关的笼统办法,如下图所示。
总体能够进行如下归类。
下面将对上述部分读办法结合源码进行阐明。
Ⅰ. get()
get() 办法用于读取一个字节,HeapByteBuffer的完结如下所示。
public byte get() {
return hb[ix(nextGetIndex())];
}
上述办法是读取字节数组中position索引方位的字节,然后position加1。再看一下DirectByteBuffer对get() 办法的完结,如下所示。
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不会改动。再看一下DirectByteBuffer对get(int) 办法的完结,如下所示。
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
上述办法是根据native办法拿到指定方位的字节,相同,position不会改动。
Ⅲ. get(byte[], int, int)
get(byte[], int, int) 办法用于将当时ByteBuffer从position方位开端往后的若干字节写入到方针字节数组的指定方位。ByteBuffer,HeapByteBuffer和DirectByteBuffer都有相应的完结,下面别离看一下。
ByteBuffer对get(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;
}
HeapByteBuffer对get(byte[], int, int) 办法的完结中,是调用System#arraycopy本地办法来进行批量复制写入,功率比一个字节一个字节的读取并写入更高,且终究会更新当时HeapByteBuffer的position。
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;
}
DirectByteBuffer对get(byte[], int, int) 办法的完结中,会先判别需求读取并写入到方针字节数组中的字节数是否大于6,大于6时会调用native办法来批量写入,否则就一个字节一个字节的读取并写入,终究还会更新当时DirectByteBuffer的position。
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[]) 办法会从当时ByteBuffer的position方位开端,读取方针字节数组长度个字节,然后顺次写入到方针字节数组。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];
}
上述办法的完结中,首先获取到当时HeapByteBuffer的position,然后从position方位开端顺次读取四个字节,由于默许状况下是大端字节序(也便是写和读都是依照大端字节序的办法),所以读取到的字节应该顺次对应int值的高位到低位,所以终究会在Bits#makeInt办法中将四个字节经过错位或的办法得到int值。
再看一下DirectByteBuffer对getInt() 办法的完结,如下所示。
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)));
}
DirectByteBuffer对getInt() 办法的全体完结思路和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办法,这儿面会有OutputStreamManager的ByteBuffer怎么被写入的相关逻辑。
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的分析能够参照下图。
为啥NIO中偏分析ByteBuffer呢,由于Netty中的缓存是ByteBuf,其对ByteBuffer做了改良,鄙人一篇文章中,将对Netty中的缓存ByteBuf进行具体分析。
假如觉得本篇文章对你有协助,求求你点个赞,加个收藏终究再点个重视吧。创作不易,感谢支撑!
本文正在参加「金石计划」