ByteBlockPool源码阅读(Lucene8.0)

1. 概述

ByteBlockPool字节块池,世界上是一个二维的byte[]lucene通过它来缓解在存储信息时需要数组扩容的问题,我们可以将其视为一个扩容性能较好的一维byte数组。其内部存储了term的倒排信息,一般包括三部分:

  • length + term:前1个或2个字节存储的length,表示后面多少个字节存储的是term信息。当IndexOptions选择不索引时,就不会保存倒排索引
  • docID + freqs:存在term文档ID以及term在文档中的频数
  • position + offsetterm在各个文档冲出现的位置(term序号,增量存储);offset是当前 term相对于上一个term的字符增量。当然如果Field支持payload,这部分也会存储paload
    这三部分不一定都有,具体要根据IndexOptions来配置。
    buffers中的存储信息大致如下图:

在这里插入图片描述
  如图,除了term信息块,其它两部都通过slice链表来存储,实现数据的不断增加。slice链表是分层的,层数增加每层的size也会增加。
  需要注意的是,ByteBlockPoolIndexWriter一样,针对一个索引是单例的,即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进行回收。步骤:

  1. 如果0填充为true,则将所有使用过的字节置0;反之什么都不做
  2. 如果重用第一行为true,则将其余行置bull;反之所有行置null;
  3. 调整游标,重用第一行则游标则为初始完成位,对象可直接使用;反之,调用完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 = 0slice,这一层级的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,并将两部分连成链表,其步骤如下:

  1. buffer中申请一块新的slice作为下一层,写入结束标志。大小结束标志层数相关;层数从0层开始,以层数为下标在LEVEL_SIZE_ARRAY数组中可以得到改层的大小,结束标志位层数加16。
  2. 原slice中的除了结束标志外最后3字节数据复制到下层slice前3字节
  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的结束呢?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值