1. 概述
ByteBlockPool字节块池,世界上是一个二维的byte[],lucene通过它来缓解在存储信息时需要数组扩容的问题,我们可以将其视为一个扩容性能较好的一维byte数组。其内部存储了term的倒排信息,一般包括三部分:
- length + term:前1个或2个字节存储的length,表示后面多少个字节存储的是term信息。当IndexOptions选择不索引时,就不会保存倒排索引。
- docID + freqs:存在term的文档的ID以及term在文档中的频数。
- position + offset:term在各个文档冲出现的位置(term序号,增量存储);offset是当前 term相对于上一个term的字符增量。当然如果Field支持payload,这部分也会存储paload。
这三部分不一定都有,具体要根据IndexOptions来配置。
buffers中的存储信息大致如下图:
如图,除了term信息块,其它两部都通过slice链表来存储,实现数据的不断增加。slice链表是分层的,层数增加每层的size也会增加。
需要注意的是,ByteBlockPool与IndexWriter一样,针对一个索引是单例的,即buffers中存储的是所有的Field域的term,可能出现相同的term,但它们是属于不同Filed域的。这样做所有的倒排信息都通过一个对象管理,方便内存占用统计以及触发flush。
2. 成员
2.1 属性
/*常量*/
//用于根据offset计算对应的buffer位置,即行下标
public final static int BYTE_BLOCK_SHIFT = 15;
//2^15 = 32768 每个buffer的字节数
public final static int BYTE_BLOCK_SIZE = 1 << BYTE_BLOCK_SHIFT;
//32767,掩码,offset对其&操作得到列下标
public final static int BYTE_BLOCK_MASK = BYTE_BLOCK_SIZE - 1;
//存储当前slice的下一层的层数,层数作为下标在LEVEL_SIZE_ARRAY 中获得下一层的大小
public final static int[] NEXT_LEVEL_ARRAY = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};
//slice的各层对应的大小
public final static int[] LEVEL_SIZE_ARRAY = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};
//第一层slice的大小
public final static int FIRST_LEVEL_SIZE = LEVEL_SIZE_ARRAY[0];
/*变量*/
//二维数组,存储数据
public byte[][] buffers = new byte[10][];
//当前buffer的下标
private int bufferUpto = -1;
//当前未分配的byte在当前buffer中的位置
public int byteUpto = BYTE_BLOCK_SIZE;
//当前buffer的引用
public byte[] buffer;
// 当前buffer的head相对于整个buffers的偏移量,每切换一次增加1个blockSize
public int byteOffset = -BYTE_BLOCK_SIZE;
//分配器,生成buffer对象,回收buffer
private final Allocator allocator;
2.2 内部类
/**分配器类抽象类,定义了生成buffer对象、回收buffer等方法*/
public abstract static class Allocator {
//buffer的大小,生成buffer对象要用
protected final int blockSize;
//构造
public Allocator(int blockSize) {
this.blockSize = blockSize;
}
//回收指定范围的buffer
public abstract void recycleByteBlocks(byte[][] blocks, int start, int end);
//回收所有buffer
public void recycleByteBlocks(List<byte[]> blocks) {
final byte[][] b = blocks.toArray(new byte[blocks.size()][]);
recycleByteBlocks(b, 0, b.length);
}
//生成buffer对象,实际就是一个byte[]
public byte[] getByteBlock() {
return new byte[blockSize];
}
}
/**分配器实现类,可以进行回收操作和内存占用记录*/
public static class DirectTrackingAllocator extends Allocator {
//内存占用计数器,从外部传入,被其它对象共享,不单单统计ByteBlockPool中的字节
private final Counter bytesUsed;
public DirectTrackingAllocator(Counter bytesUsed) {
this(BYTE_BLOCK_SIZE, bytesUsed);
}
public DirectTrackingAllocator(int blockSize, Counter bytesUsed) {
super(blockSize);
this.bytesUsed = bytesUsed;
}
@Override
public byte[] getByteBlock() {
bytesUsed.addAndGet(blockSize);
return new byte[blockSize];
}
@Override//回收blocks中指定范围的block
public void recycleByteBlocks(byte[][] blocks, int start, int end) {
//内存占用量减少,数组释放
bytesUsed.addAndGet(-((end-start)* blockSize));
for (int i = start; i < end; i++) {
blocks[i] = null;
}
}
};
/**一个简单的分配器实现类,不回收、不记录内存占用*/
public static final class DirectAllocator extends Allocator {
public DirectAllocator() {
this(BYTE_BLOCK_SIZE);
}
public DirectAllocator(int blockSize) {
super(blockSize);
}
@Override
public void recycleByteBlocks(byte[][] blocks, int start, int end) {
}
}
每个DocumentsWriterPerThread(dwpt)对象对应一个DirectTrackingAllocator分配器,传入一个计数器,来统计内存占用,计数器也会传给其它对象,共同记录内存占用。
3. 辅助方法
3.1 buffer切换
//buffer游标切换,如果已经是最后一行则需要二维数组扩容
public void nextBuffer() {
if (1+bufferUpto == buffers.length) {//
//NUM_BYTES_OBJECT_REF:当前JVM用多少个字节表示一个对象引用,JRE64位开启引用压缩,为4,没开8.
//JRE非64位则4,数组扩容策略是:
//newlength =(delta=(lenth+1)/8 >3?length+1+delta:length +1+3);然后根据JRE以及引用字节数决定,对newLength
//是原样返回还是对其补成2,4,8的整倍数后返回
byte[][] newBuffers = new byte[ArrayUtil.oversize(buffers.length+1,
NUM_BYTES_OBJECT_REF)][];
System.arraycopy(buffers, 0, newBuffers, 0, buffers.length);
buffers = newBuffers;
}
//当前二维数组的buffer赋值,getByteBlock是返回一个blocksize=32768大小的byte[].
buffer = buffers[1+bufferUpto] = allocator.getByteBlock();
//当前buffer+1
bufferUpto++;
//当前byte归0
byteUpto = 0;
//byte偏移量,实例化时为-BYTE_BLOCK_SIZE,实例化后调一次偏移量变为0.
byteOffset += BYTE_BLOCK_SIZE;
}
对buffers数组的扩容量根据jre的位数以及配置相关。
3.2 ByteBlockPool对象重置
public void reset(boolean zeroFillBuffers, boolean reuseFirst) {
if (bufferUpto != -1) {//buffers已经用过
// We allocated at least one buffer
if (zeroFillBuffers) {//要求0填充
for(int i=0;i<bufferUpto;i++) {//0填充,当前行之前的所有行
// Fully zero fill buffers that we fully used
Arrays.fill(buffers[i], (byte) 0);
}
// Partial zero fill the final buffer,填充当前行已使用的部分
Arrays.fill(buffers[bufferUpto], 0, byteUpto, (byte) 0);
}
if (bufferUpto > 0 || !reuseFirst) {
final int offset = reuseFirst ? 1 : 0;
// Recycle all but the first buffer,回收,allocator如果是DirectTrackingAllocator
//则其调用回收方法已经完成回收,不必调用fill方法
allocator.recycleByteBlocks(buffers, offset, 1+bufferUpto);
//确保无论分配器有没有回收算法,都会被set为null
Arrays.fill(buffers, offset, 1+bufferUpto, null);
}
//设置游标
if (reuseFirst) {//重复利用首行,在调用此方法之后就不需调nextBuffer便可直接用
// Re-use the first buffer
bufferUpto = 0;
byteUpto = 0;
byteOffset = 0;
buffer = buffers[0];
} else {
bufferUpto = -1;
byteUpto = BYTE_BLOCK_SIZE;
byteOffset = -BYTE_BLOCK_SIZE;
buffer = null;
}
}
}
此方法用于对字节块池的重置,主要作用是对buffer进行回收。步骤:
- 如果0填充为true,则将所有使用过的字节置0;反之什么都不做
- 如果重用第一行为true,则将其余行置bull;反之所有行置null;
- 调整游标,重用第一行则游标则为初始完成位,对象可直接使用;反之,调用完reset方法后需要调用一下nextBuffer方法初始化游标。
4.写入的方法
4.1 newSlice 分配新的slice块
slice链表头部的分配:
public int newSlice(final int size) {
if (byteUpto > BYTE_BLOCK_SIZE-size)//当前buffer的剩余量不够,切换新的buffer
nextBuffer();
final int upto = byteUpto;
//当前列游标位置变化
byteUpto += size;
//slice末尾标记为16
buffer[byteUpto-1] = 16;
//返回起始位
return upto;
}
这个方法用于在buffer中分配level = 0的slice,这一层级的slice的大小为5,结束标志位16。所谓分配实际上就是在末尾位置写上结束标志。注意此方法在分配过程中不支持跨buffer,即分配的slice块都在一个buffer中,因此,buffer并不紧凑。
4.2 allocSlice 连接slice
用slice生成并连接下一层slice
public int allocSlice(final byte[] slice, final int upto) {
//结束符对16求余为当前层数,
final int level = slice[upto] & 15;
//下层层数,1、2、3...
final int newLevel = NEXT_LEVEL_ARRAY[level];
//下层slice大小 1层size=14,...
final int newSize = LEVEL_SIZE_ARRAY[newLevel];
// Maybe allocate another block,block不够则换行
if (byteUpto > BYTE_BLOCK_SIZE-newSize) {
nextBuffer();
}
final int newUpto = byteUpto;
//当前slice在buffers中的offset,
final int offset = newUpto + byteOffset;
//列指针移动
byteUpto += newSize;
// Copy forward the past 3 bytes (which we are about
// to overwrite with the forwarding address):
//将结束符前三位拷贝到下层slice的前三位
buffer[newUpto] = slice[upto-3];
buffer[newUpto+1] = slice[upto-2];
buffer[newUpto+2] = slice[upto-1];
// Write forwarding address at end of last slice:
//原slice拷贝过的三位连同结束符一共4位,表示下一级slice在buffers中的offset,注意offset是结束符后一位,不是slice首位。
slice[upto-3] = (byte) (offset >>> 24);
slice[upto-2] = (byte) (offset >>> 16);
slice[upto-1] = (byte) (offset >>> 8);
slice[upto] = (byte) offset;
// Write new level:写入新slice的结束符:16+层数
buffer[byteUpto-1] = (byte) (16|newLevel);
//返回新slice的空位(前三个字节已经写入)
return newUpto+3;
}
当slice写满时,需要申请下一层的slice,并将两部分连成链表,其步骤如下:
- 在buffer中申请一块新的slice作为下一层,写入结束标志。大小和结束标志与层数相关;层数从0层开始,以层数为下标在LEVEL_SIZE_ARRAY数组中可以得到改层的大小,结束标志位层数加16。
- 将原slice中的除了结束标志外最后3字节数据复制到下层slice的前3字节。
- 将下层slice的位置(offset)写入到原slice包括结束标志在内的4个字节,完成链表的连接。
注意,传入的应该是原slice所在buffer和原slice结束标志的下标;这一方法也不支持跨行。
4.3 append写入指定数据
将指定的数据写入到当前buffer的当前位置:
public void append(final BytesRef bytes) {
int bytesLeft = bytes.length;
int offset = bytes.offset;
while (bytesLeft > 0) {
int bufferLeft = BYTE_BLOCK_SIZE - byteUpto;
if (bytesLeft < bufferLeft) {
// fits within current buffer
System.arraycopy(bytes.bytes, offset, buffer, byteUpto, bytesLeft);
byteUpto += bytesLeft;
break;
} else {
// fill up this buffer and move to next one
if (bufferLeft > 0) {
System.arraycopy(bytes.bytes, offset, buffer, byteUpto, bufferLeft);
}
nextBuffer();
bytesLeft -= bufferLeft;
offset += bufferLeft;
}
}
}
将BytesRef表示的数据写入到buffer中,支持跨buffer写入。
5. 读取相关的方法
在从buffer中读取数据的方法中,有的会跨行读取,有的不会;有的存在数组拷贝,而有的仅仅是引用原有buffer,需要注意。(只要跨行读取,则无法直接引用buffer,需要数组拷贝)
5.1 readByte读取单个字节
public byte readByte(long offset) {
int bufferIndex = (int) (offset >> BYTE_BLOCK_SHIFT);//整除32768获得buffer
int pos = (int) (offset & BYTE_BLOCK_MASK);
byte[] buffer = buffers[bufferIndex];
return buffer[pos];
}
返回指定单个字节
5.2 readBytes数组拷贝
将buffer中指定范围的数据拷贝到指定的字节数组的指定范围,允许跨buffer拷贝。
public void readBytes(final long offset, final byte bytes[], int bytesOffset, int bytesLength) {
//剩余填充长度
int bytesLeft = bytesLength;
//行标
int bufferIndex = (int) (offset >> BYTE_BLOCK_SHIFT);
//列标
int pos = (int) (offset & BYTE_BLOCK_MASK);
while (bytesLeft > 0) {//剩余填充长度>0 循环次数1-2
//对应行buffer
byte[] buffer = buffers[bufferIndex++];
//本行读取长度(一般情况下,bytesLeft<bufferLeft,首次循环执行完结束;反之,需要跨行读取,进入第二次循环)
int chunk = Math.min(bytesLeft, BYTE_BLOCK_SIZE - pos);
System.arraycopy(buffer, pos, bytes, bytesOffset, chunk);
bytesOffset += chunk;
bytesLeft -= chunk;
pos = 0;
}
}
全为数组拷贝,没有引用,允许跨行。offset用来指定读取起始位置。
5.3 setRawBytesRef读取指定数据
public void setRawBytesRef(BytesRef ref, final long offset) {
//计算buffer位置在buffers中的位置,行位置
int bufferIndex = (int) (offset >> BYTE_BLOCK_SHIFT);
//计算byte在buffer中的位置,列位置
int pos = (int) (offset & BYTE_BLOCK_MASK);
if (pos + ref.length <= BYTE_BLOCK_SIZE) {//不跨行
ref.bytes = buffers[bufferIndex];//
ref.offset = pos;
} else {//跨行
ref.bytes = new byte[ref.length];
ref.offset = 0;
//跨行拷贝
readBytes(offset, ref.bytes, 0, ref.length);
}
}
将buffer中的数据读取到BytesRef中,BytesRef需要指定拷贝长度,无需指定偏移量。当读取范围不跨buffer时,直接引用相应的buffe,无需数组拷贝;反之,将目标数据拷贝到一个新的字节数组中,然后引用新的数组。即:支持跨buffer,此时是数组拷贝后引用新数组。
5.4
void setBytesRef(BytesRefBuilder builder, BytesRef result, long offset, int length) {
result.length = length;
//计算位置
int bufferIndex = (int) (offset >> BYTE_BLOCK_SHIFT);
byte[] buffer = buffers[bufferIndex];
int pos = (int) (offset & BYTE_BLOCK_MASK);
//是否跨buffer
if (pos + length <= BYTE_BLOCK_SIZE) {//不跨行
// common case where the slice lives in a single block: just reference the buffer directly without copying
result.bytes = buffer;
result.offset = pos;
} else {//跨行
// uncommon case: the slice spans at least 2 blocks, so we must copy the bytes:
builder.grow(length);
result.bytes = builder.get().bytes;
result.offset = 0;
readBytes(offset, result.bytes, 0, length);
}
}
与setRawBytesRef方法作用一样。只不过在跨buffer读取时,新生成的字节数组由builder获得,拷贝数据后被引用。
5.5 setBytesRef
public void setBytesRef(BytesRef term, int textStart) {
//行
final byte[] bytes = term.bytes = buffers[textStart >> BYTE_BLOCK_SHIFT];
//列
int pos = textStart & BYTE_BLOCK_MASK;
if ((bytes[pos] & 0x80) == 0) {//0xxx xxxx 直接就是长度
// length is 1 byte
term.length = bytes[pos];
term.offset = pos+1;
} else {//1xxx xxxx 连同后一个字节共同表示长度,需要解码
// length is 2 bytes 去第1字个节前7位为低位y 第二个字节为高8位x: xxxx xxxx yyyy yyy
term.length = (bytes[pos]&0x7f) + ((bytes[pos+1]&0xff)<<7);
term.offset = pos+2;
}
assert term.length >= 0;
}
此方法用于读取指定词,textStart指明表示长度的数据位置,长度的编码占1-2个字节,解码后其后面对应的长度范围就是term的数据,读取的就是这部分数据,这里不支持跨buffer读取,都是直接引用buffer,不存在拷贝。
6.总结
可以将ByteBlockPool可以视为一个扩容性能较好的以为字节数组,不过在分配时不完全紧凑。
- slice有下一层时,本层的slice结束标志会被抹掉,用来存储下一层的位置,那么如何判断本层slice的结束呢?