【Netflix Hollow系列】深入Hollow的内存实现以及ByteBuffer的应用

526 阅读26分钟

前言

在前序的文章中,已经从大范围上介绍了Hollow的内存模型,具体的文章为:【Netflix Hollow系列】深入分析Hollow内存布局,本文将在此基础上针对Hollow的momory模块的计算逻辑和实现方法,逐行分析解读。

希望能够帮助大家充分的理解Hollow的内存模型,同时也能在遇到高并发高性能要求的场景下,有更多的选择和优化方案。@空歌白石

体系结构

继承关系

首先从整体上了解下Hollow内存模型的继承关系,可以看出整体上分为variablefixed(可变和固定长度)两种数据结构类型。其中variablebyte[][]存储,fixedlong[][]存储。具体的细节将在下文中阐述。

Hollow-Byte继承关系.drawio.png

MemoryMode

Hollow 将内存模型分为ON_HEAPSHARED_MEMORY_LAZY两种,预留第三种定义SHARED_MEMORY_EAGER,方式目前并未实现。枚举定义如下:

  1. ON_HEAP:立即加载到主内存,在JVM堆上,自定义二维byte或long数组
  2. SHARED_MEMORY_LAZY:映射到虚拟内存和延迟加载到主内存,基于JDK的ByteBuff实现。
  3. SHARED_MEMORY_EAGER:(将来)映射到虚拟内存,并立即加载到主内存,离开堆

Hollow基于ByteBuf的实现上,针对上述两种不同的MemoryMode有两种不同的实现,通过方法名可以区分出来,以Encoded开头的使用的SHARED_MEMORY_LAZY模式,以ElementArray结尾的使用的ON_HEAP模式。

读取

为什么会有可变和固定长度的数据结构呢?首先看下HollowObjectTypeDataElements是如何应用的:

// 空歌白石:省略无关代码

FixedLengthData fixedLengthData;
final VariableLengthData varLengthData[];

GapEncodedVariableLengthIntegerReader encodedAdditions;
GapEncodedVariableLengthIntegerReader encodedRemovals;

// 空歌白石:省略无关代码

public HollowObjectTypeDataElements(HollowObjectSchema schema, ArraySegmentRecycler memoryRecycler) {
    this(schema, MemoryMode.ON_HEAP, memoryRecycler);
}

public HollowObjectTypeDataElements(HollowObjectSchema schema, MemoryMode memoryMode, ArraySegmentRecycler memoryRecycler) {
    varLengthData = new VariableLengthData[schema.numFields()];

    // 空歌白石:省略无关代码
}

void readSnapshot(HollowBlobInput in, HollowObjectSchema unfilteredSchema) throws IOException {
    readFromInput(in, false, unfilteredSchema);
}

void readDelta(HollowBlobInput in) throws IOException {
    readFromInput(in, true, schema);
}

void readFromInput(HollowBlobInput in, boolean isDelta, HollowObjectSchema unfilteredSchema) throws IOException {
    maxOrdinal = VarInt.readVInt(in);

    if (isDelta) {
        encodedRemovals = GapEncodedVariableLengthIntegerReader.readEncodedDeltaOrdinals(in, memoryRecycler);
        encodedAdditions = GapEncodedVariableLengthIntegerReader.readEncodedDeltaOrdinals(in, memoryRecycler);
    }

    readFieldStatistics(in, unfilteredSchema);

    fixedLengthData = FixedLengthDataFactory.get(in, memoryMode, memoryRecycler);
    removeExcludedFieldsFromFixedLengthData();

    readVarLengthData(in, unfilteredSchema);
}

VariableLengthData按照HollowObjectSchema的字段数量分配空间,可以认为每个具体的Schema的Filed都存储在可变长度的内存空间,最终从HollowBlobInput输入流中读取可变长度的数据。

FixedLengthDataHollowBlobInput输入流中读取固定长度的数据。

写入

以上介绍了Hollow读数据的方式,接下来我们看下Hollow如何写数据?以Object类型的实现HollowObjectTypeWriteState为例说明。

@Override
public void calculateSnapshot() {
    maxOrdinal = ordinalMap.maxOrdinal();
    int numBitsPerRecord = fieldStats.getNumBitsPerRecord();
    
    fixedLengthLongArray = new FixedLengthElementArray[numShards];
    varLengthByteArrays = new ByteDataArray[numShards][];
    recordBitOffset = new long[numShards];
    
    for(int i=0;i<numShards;i++) {
        fixedLengthLongArray[i] = new FixedLengthElementArray(WastefulRecycler.DEFAULT_INSTANCE, (long)numBitsPerRecord * (maxShardOrdinal[i] + 1));
        varLengthByteArrays[i] = new ByteDataArray[getSchema().numFields()];
    }
    
    int shardMask = numShards - 1;

    for(int i=0;i<=maxOrdinal;i++) {
        int shardNumber = i & shardMask;
        if(currentCyclePopulated.get(i)) {
            addRecord(i, recordBitOffset[shardNumber], fixedLengthLongArray[shardNumber], varLengthByteArrays[shardNumber]);
        } else {
            addNullRecord(i, recordBitOffset[shardNumber], fixedLengthLongArray[shardNumber], varLengthByteArrays[shardNumber]);
        }
        recordBitOffset[shardNumber] += numBitsPerRecord;
    }
}

private void addRecord(int ordinal, long recordBitOffset, FixedLengthElementArray fixedLengthLongArray, ByteDataArray varLengthByteArrays[]) {
    long pointer = ordinalMap.getPointerForData(ordinal);

    for(int fieldIndex=0; fieldIndex < getSchema().numFields(); fieldIndex++) {
        pointer = addRecordField(pointer, recordBitOffset, fieldIndex, fixedLengthLongArray, varLengthByteArrays);
    }
}

private long addRecordField(long readPointer, long recordBitOffset, int fieldIndex, FixedLengthElementArray fixedLengthLongArray, ByteDataArray varLengthByteArrays[]) {
    FieldType fieldType = getSchema().getFieldType(fieldIndex);
    long fieldBitOffset = recordBitOffset + fieldStats.getFieldBitOffset(fieldIndex);
    int bitsPerElement = fieldStats.getMaxBitsForField(fieldIndex);
    ByteData data = ordinalMap.getByteData().getUnderlyingArray();

    switch(fieldType) {
    case BOOLEAN:
        if(VarInt.readVNull(data, readPointer)) {
            fixedLengthLongArray.setElementValue(fieldBitOffset, 2, 3);
        } else {
            fixedLengthLongArray.setElementValue(fieldBitOffset, 2, data.get(readPointer));
        }
        readPointer += 1;
        break;
    case FLOAT:
        long intValue = data.readIntBits(readPointer) & 0xFFFFFFFFL;
        fixedLengthLongArray.setElementValue(fieldBitOffset, 32, intValue);
        readPointer += 4;
        break;
    case DOUBLE:
        long longValue = data.readLongBits(readPointer);
        fixedLengthLongArray.setElementValue(fieldBitOffset, 64, longValue);
        readPointer += 8;
        break;
    case LONG:
    case INT:
    case REFERENCE:
        if(VarInt.readVNull(data, readPointer)) {
            fixedLengthLongArray.setElementValue(fieldBitOffset, bitsPerElement, fieldStats.getNullValueForField(fieldIndex));
            readPointer += 1;
        } else {
            long vLong = VarInt.readVLong(data, readPointer);
            fixedLengthLongArray.setElementValue(fieldBitOffset, bitsPerElement, vLong);
            readPointer += VarInt.sizeOfVLong(vLong);
        }
        break;
    case BYTES:
    case STRING:
        ByteDataArray varLengthBuf = getByteArray(varLengthByteArrays, fieldIndex);

        if(VarInt.readVNull(data, readPointer)) {
            long offset = varLengthBuf.length();

            fixedLengthLongArray.setElementValue(fieldBitOffset, bitsPerElement, offset | (1L << (bitsPerElement - 1))); // write offset with set null bit
            readPointer += 1;
        } else {
            int length = VarInt.readVInt(data, readPointer);
            readPointer += VarInt.sizeOfVInt(length);
            varLengthBuf.copyFrom(data, readPointer, length);

            long offset = varLengthBuf.length();

            fixedLengthLongArray.setElementValue(fieldBitOffset, bitsPerElement, offset);
            readPointer += length;
        }
        break;
    }
    return readPointer;
}

private ByteDataArray getByteArray(ByteDataArray buffers[], int index) {
    if(buffers[index] == null) {
        buffers[index] = new ByteDataArray(WastefulRecycler.DEFAULT_INSTANCE);
    }
    return buffers[index];
}

通过写入代码可以清晰看出,Hollow会将BOOLEANFLOATDOUBLELONGINTREFERENCE数据写入到FixedLengthElementArray中,而BYTESSTRING的具体数据会在ByteDataArray存储,在FixedLengthElementArray中存储offset在ByteDataArray中的offset

public enum MemoryMode {

    ON_HEAP,                // eager load into main memory, on JVM heap
    SHARED_MEMORY_LAZY;     // map to virtual memory and lazy load into main memory, off heap
    // SHARED_MEMORY_EAGER  // (in future) map to virtual memory and eager load into main memory, off heap

    /*
     * Returns whether a memory mode is supported by Hollow consumer
     */
    public boolean consumerSupported() {
        return this.equals(ON_HEAP) || this.equals(SHARED_MEMORY_LAZY);
    }

    /*
     * Returns whether a memory mode supports type filtering
     */
    public boolean supportsFiltering() {
        return this.equals(ON_HEAP);
    }
}

JDK ByteBuffer

ByteBuffer是什么?JDK的解释是A byte buffer.,就是一个字节缓冲区。首先看下JDK中ByteBuffer的继承关系。ByteBuffer继承自`Buffer

Buffer.png

ByteBuffer的实现分为两类:DirectHeap

  1. HeapByteBuffer:
    • 在JVM堆上面的一个buffer,底层的本质是一个数组。
    • 由于Buffer维护在JVM里,所以把内容写进buffer里速度会快些;并且,可以更容易回收
  2. DirectByteBuffer:
    • 底层的数据其实是维护在操作系统的内存中,而不是JVM里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据。
    • 跟外设(IO设备)打交道时会快很多,因为外设读取JVM堆里的数据时,不是直接读取的,而是把JVM里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy。
    • 为提升从操作系统与JVM之间数据交换的速率,提供了一个映射的类MappedByteBuffer

ByteBuffer的构造方法:

// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
            byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

// Creates a new buffer with the given mark, position, limit, and capacity
//
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
    this(mark, pos, lim, cap, null, 0);
}

通过以上构造方法可以看出,ByteBuffer有如下核心的参数:

  1. mark
    • 为某一读过的位置做标记,便于某些时候回退到该位置。
  2. position
    • 当前读取的位置。
  3. limit
    • 当写数据到buffer中时,limit一般和capacity相等
    • 当读数据时,limit代表buffer中有效数据的长度。
  4. capacity
    • 初始化时候的容量。
  5. byte[] buff
    • buff即内部用于缓存的数组。
    • 默认赋值为null。
  6. offset
    • buff的偏移量。
    • 初始偏移量为0。

以上这些属性总是满足以下条件:0 <= mark <= position <= limit <= capacity,这些概念在Hollow的实现中也会经常用到。

Buffer的作用

Buffer顾名思义,是缓冲区的意思,使用buffer可以带来很多好处:

  • 减少实际的物理读写次数。
  • 缓冲区创建时分配固定内存,这块内存区域可被重用,减少动态分配和回收内存的次数。

ByteData

Hollow使用BitPool也就是池化的思想来维护bytebuffer,其中ByteData用于隐藏字节范围的底层实现。ByteData的继承关系如下图:

ByteData.png

源码

ByteData有default的read方法:readLongBitsreadIntBits

default long readLongBits(long position) {
    long longBits = (long) (get(position++) & 0xFF) << 56;
    longBits |= (long) (get(position++) & 0xFF) << 48;
    longBits |= (long) (get(position++) & 0xFF) << 40;
    longBits |= (long) (get(position++) & 0xFF) << 32;
    longBits |= (long) (get(position++) & 0xFF) << 24;
    longBits |= (get(position++) & 0xFF) << 16;
    longBits |= (get(position++) & 0xFF) << 8;
    longBits |= (get(position) & 0xFF);
    return longBits;
}

default int readIntBits(long position) {
    int intBits = (get(position++) & 0xFF) << 24;
    intBits |= (get(position++) & 0xFF) << 16;
    intBits |= (get(position++) & 0xFF) << 8;
    intBits |= (get(position) & 0xFF);
    return intBits;
}

read方法依赖的get方法需要由各个接口实现类自行实现。

/**
* Get the value of the byte at the specified position.
* @param index the position (in byte units)
* @return the byte value
*/
byte get(long index);

此外,ByteData还有一个length()方法,默认为default,只有ArrayByteData子类具体实现了此方法。

default long length() {
    throw new UnsupportedOperationException();
}

0xFF

0x表示十六进制,0xFF表示为15 * 16 + 15 = 255

对应的二进制,就是8个1,即1111 1111,也就是255

byte 为什么要 & 0xff?

get方法的返回是byte。在readLongBitsreadIntBits方法中,会有get(position) & 0xFF的计算,也就是byte & 0xFF,这又是为什么呢?

byte是8位二进制,0xFF转化成8位二进制是1111 1111,那么byte & 0xFF不是还是byte本身吗?这样做有意思吗?

计算机内的存储都是利用二进制的补码进行存储的。

  • 对于正数00000001原码来说,首位表示符号位,反码、补码都是本身。
  • 对于负数100000001原码来说,反码是对原码除了符号位之外作取反运算即111111110,补码是对反码作+1运算即111111111

假设byte为-127,byte作为一个byte类型,其计算机存储的补码是10000001(8位)。 将byte作为int类型向控制台输出的时候,jvm作了一个补位的处理,因为int类型是32位所以补位后的补码就是1111111111111111111111111 10000001(32位),这个32位二进制补码表示的也是-127。发现没有,虽然byte->int计算机背后存储的二进制补码由10000001(8位)转化成了1111111111111111111111111 10000001(32位)很显然这两个补码表示的十进制数字依然是相同的。

但是ByteDatabyte->intbyte->long的转化,只是为了保持十进制的一致性吗?不一定吧?好比我们拿到的文件流转成byte数组,难道我们关心的是byte数组的十进制的值是多少吗?我们关心的是其背后二进制存储的补码吧。所以大家应该能猜到为什么byte类型的数字要&0xff再赋值给int类型,其本质原因就是想保持二进制补码的一致性。

当byte要转化为int的时候,高的24位必然会补1,这样,其二进制补码其实已经不一致了,&0xff可以将高的24位置为0,低8位保持原样。这样做的目的就是为了保证二进制数据的一致性。当然拉,保证了二进制数据性的同时,如果二进制被当作byte和int来解读,其10进制的值必然是不同的,因为符号位位置已经发生了变化。

有人问为什么上面的式子中a[0]不是8位而是32位,因为当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,也就是进行get(position) & 0xFF运算,就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位,再参与运算。上面的0xff其实是int类型的字面量值,所以可以说byte与int进行运算。

向左移位

long longBits = (long) (get(position++) & 0xFF) << 56; 这行代码是做什么呢?

左移操作<<可以转换为 * 2^n的计算,因此以上代码表示,获取到具体position++位的byte值,首先使用0xFF将存储的计算机补码还原,再将byte转为long,最后将long值与2的56次方相乘,进而得到一个新的long值。

或运算

在经过56的左移运算后,计算得到的longBits,需要与48左移的结果进行或|运算,后续不断的与403224168等的左移运算结果进行或运算。

position++:每进行一次或运算,position都要进行++运算,目的是为了移动到下一个byte位。

首先复习下或运算的计算规则。

运算规则:
0|0=0
0|1=1
1|0=1
1|1=1
即: 参加运算的两个对象,一个为1,其值为1

由于先进行了左移位运算,因此或运算后,会将对应的8位byte值存储在对应的32位int或64位long的对应位置。

总结

经过补码转换,左移、或运算、移动到下一个byte位,最终,可以从一个byte数组中从一个给定position开始,将byte数据转为int或long类型的数据。为后续ByteBuf的使用提供了支撑。

一句话总结ByteDatareadLongBitsreadIntBits两个方法:将8个byte存储在一个long,将4个byte存储在一个int。

ArrayByteData

ArrayByteData的结构很简单,就是由一个简单字节数组实现的ByteData,负责存储具体的byte值。

public class ArrayByteData implements ByteData {

    private final byte[] data;

    public ArrayByteData(byte[] data) {
        this.data = data;
    }

    @Override
    public byte get(long position) {
        return data[(int)position];
    }

    @Override
    public long length() {
        return data.length;
    }
}

VariableLengthData

从概念上讲,VariableLengthData可以被认为是一个单字节数组或未定义长度的缓冲区。当一个字节写入大于当前分配的数组/缓冲区的索引时,它将自动增长。包含4个接口方法:loadFrom.copy.orderedCopy.size.

public interface VariableLengthData extends ByteData {

    /**
     * Load <i>length</i> bytes of data from the supplied {@code HollowBlobInput}
     * 空歌白石:从HollowBlobInput中加载指定length的字节到缓冲区。
     *
     * @param in the {@code HollowBlobInput}
     * @param length the length of the data to load
     * @throws IOException if data could not be loaded
     */
    void loadFrom(HollowBlobInput in, long length) throws IOException;

    /**
     * Copy bytes from another {@code VariableLengthData} object.
     * 空歌白石:从另外一个VariableLengthData中赋值字节数组到当前的VariableLengthData缓冲区。
     *
     * @param src the source {@code VariableLengthData}
     * @param srcPos position in source data to begin copying from
     * @param destPos position in destination data to begin copying to
     * @param length length of data to copy in bytes
     */
    void copy(ByteData src, long srcPos, long destPos, long length);

    /**
     * Copies data from the provided source into destination, guaranteeing that if the update is seen
     * by another thread, then all other writes prior to this call are also visible to that thread.
     * 空歌白石:将数据从提供的源复制到目标,确保如果更新被另一个线程看到,那么在此调用之前的所有其他写入对该线程也是可见的。这个方法名取的有些比较难理解,在下文中分析到具体的orderedCopy实现后应该会有助于理解。主要区别是是否线程安全。
     *
     * @param src the source data
     * @param srcPos position in source data to begin copying from
     * @param destPos position in destination to begin copying to
     * @param length length of data to copy in bytes
     */
    void orderedCopy(VariableLengthData src, long srcPos, long destPos, long length);

    /**
     * Data size in bytes
     * @return size in bytes
     */
    long size();
}

EncodedByteBuffer

EncodedByteBuffer是基于 BlobByteBuffer 的可变长度字节数据的实现,仅支持读取。BlobByteBuffer负责所有在内存中数据的存储,BlobByteBuffer实现了将类似于BLOB这样的大文件和MappedByteBuffer之间的桥梁,这里只是简单提下,BlobByteBuffer将在下文详细阐述。

  • get方法使用BlobByteBuffergetByte方法从buffer中获取通过给定的index以及当前bufer所在position计算得到的索引位置。获取指定的byte。
  • HollowBlobInput的buffer引用了BlobByteBuffer,因此,loadFrom方法中基于BlobByteBuffer构建了一个bufferView
public class EncodedByteBuffer implements VariableLengthData {

    private BlobByteBuffer bufferView;
    private long size;

    public EncodedByteBuffer() {
        this.size = 0;
    }

    @Override
    public byte get(long index) {
        if (index >= this.size) {
            throw new IllegalStateException();
        }

        byte retVal = this.bufferView.getByte(this.bufferView.position() + index);
        return retVal;
    }

    /**
     * {@inheritDoc}
     * This is achieved by initializing a {@code BlobByteBuffer} that is a view on the underlying {@code BlobByteBuffer}
     * and advancing the position of the underlying buffer by <i>length</i> bytes.
     *
     * 空歌白石:这是通过初始化一个 {@code BlobByteBuffer} 来实现的,该 {@code BlobByteBuffer} 是底层 {@code BlobByteBuffer} 的一个视图,并将底层缓冲区的位置推进 <i>length</i> 个字节。
     */
    @Override
    public void loadFrom(HollowBlobInput in, long length) throws IOException {
        
        // 空歌白石:从HollowBlobInput取出buffer
        BlobByteBuffer buffer = in.getBuffer();

        // 空歌白石:赋值当前bufffer的长度
        this.size = length;

        // 空歌白石:`getFilePointer`返回此输入中的当前偏移量,在该偏移量处发生下一次读取。赋值到当前的buffer位置中。
        buffer.position(in.getFilePointer());

        // 空歌白石:将buffer赋值一份,用于`EncodedByteBuffer`的只读。`EncodedByteBuffer`仅仅支持只读的原因也就在于此。
        this.bufferView = buffer.duplicate();

        // 空歌白石:调整`HollowBlobInput`的position,便于下一次load
        buffer.position(buffer.position() + length);

        // 空歌白石:通过将调用中继到底层 {@code RandomAccessFile},将文件指针设置为从文件开头测量的所需偏移量。如果Hollow Blob 输入是 {@code DataInputStream},则不支持操作。
        in.seek(in.getFilePointer() + length);
    }

    @Override
    public void copy(ByteData src, long srcPos, long destPos, long length) {
        throw new UnsupportedOperationException("Operation not supported in shared-memory mode");
    }

    @Override
    public void orderedCopy(VariableLengthData src, long srcPos, long destPos, long length) {
        throw new UnsupportedOperationException("Operation not supported in shared-memory mode");
    }

    @Override
    public long size() {
        return size;
    }
}

这里有个疑问,为什么EncodedByteBuffer的命名中会用Encoded修饰呢?我觉得可以这样理解,这里的encoded指的是将一条record中的每一个filed编码的意思,方便最终以byte形式存储在内存。

SegmentedByteArray

SegmentedByteArray是一个使用分段字节数组对 ByteData 接口的实现,这些数组段可能来自可重用内存池。SegmentedByteArray可以在不连续分配更大的块和复制内存的情况下增长。SegmentedByteArray中每段长度始终是 2的幂,因此可以通过掩码和移位操作找到给定索引的位置。

从概念上讲,这可以被认为是一个未定义长度的单字节数组,当前分配的缓冲区将始终是段大小的倍数,当一个字节写入大于当前分配的缓冲区的索引时,缓冲区将自动增长。

构造方法和属性

// 空歌白石:用于存储分段二维字节数组,第一维度表示分的segment,第二维度表示具体的数据。
private byte[][] segments;

// 空歌白石:每段大小的log2长度
private final int log2OfSegmentSize;

// 空歌白石:每一位的掩码
private final int bitmask;

// 空歌白石:分段数组回收站,用于内存的重新分配
private final ArraySegmentRecycler memoryRecycler;

public SegmentedByteArray(ArraySegmentRecycler memoryRecycler) {
    // 空歌白石:二维数组初始化长度为2
    this.segments = new byte[2][];
    
    // 空歌白石:从内存回收站得到的分段大小
    this.log2OfSegmentSize = memoryRecycler.getLog2OfByteSegmentSize();

    // 空歌白石:分选数组的掩码
    this.bitmask = (1 << log2OfSegmentSize) - 1;
    
    // 空歌白石:赋值回收站或者说内存池
    this.memoryRecycler = memoryRecycler;
}

SegmentedByteArray定义了一个名为segmentsbyte[][]二维数组。

可能大家会想到,会什么要用二维数组而不用一维数组呢?这里有两方面原因:

  1. 使用二维数组可以有效地提升读写性能,其中两个维度分别为index >> log2OfSegmentSizeindex & bitmask计算
  2. 可以有效的减少内存的占用,利于回收站memoryRecycler的维护。

set

/**
* Set the byte at the given index to the specified value
* 空歌白石:将给定索引处的字节设置为指定值
*
* @param index the index
* @param value the byte value
*/
public void set(long index, byte value) {
    int segmentIndex = (int)(index >> log2OfSegmentSize);
    ensureCapacity(segmentIndex);
    segments[segmentIndex][(int)(index & bitmask)] = value;
}

可以通过下图更直观的查看set方法的作用。

Hollow-SegmentedByteArray.drawio.png

ensureCapacity

在上述set方法中,当获取到具体的segmentIndex后,首先会判断当前segments数组是否满足大小,如果不满足,需要扩容数组,也就扩容了缓存区。

缓存区的扩容比例:segments.length * 3 / 2,也可以理解为按照1.5倍扩容。对于新扩容的segments,需要的内存空间是从回收站获取的。数组回收站的获取有两种实现:

  1. 一种是直接扩容
  2. 一种是利用现有空闲空闲

这部分逻辑将在ArraySegmentRecycler部分阐述。

/**
* Ensures that the segment at segmentIndex exists
*
* @param segmentIndex the segment index
*/
private void ensureCapacity(int segmentIndex) {
    while(segmentIndex >= segments.length) {
        segments = Arrays.copyOf(segments, segments.length * 3 / 2);
    }

    if(segments[segmentIndex] == null) {
        segments[segmentIndex] = memoryRecycler.getByteArray();
    }
}

get

getset的逆向方法,相对来说比较简单了,直接通过index可以获取到对应的byte数组。

/**
* Get the value of the byte at the specified index.
* @param index the index
* @return the byte value
*/
@Override
public byte get(long index) {
    return segments[(int)(index >>> log2OfSegmentSize)][(int)(index & bitmask)];
}

应该有注意到一点,在set时,寻址使用的index >> log2OfSegmentSize,而在get时,寻址使用的index >>> log2OfSegmentSize,这是为什么呢?

首先看下java中有三种移位运算符:

  • << :左移运算符。例如:num << 1,相当于num乘以2
  • >> : 右移运算符。例如:num >> 1,相当于num除以2
  • >>> : 无符号右移,忽略符号位,空位都以0补齐。例如:num >> 1,指定要移位值num移动的1位。
    • 无符号右移的规则只记住一点:忽略了符号位扩展,0补最高位,无符号右移运算符>>>,只是对32位和64位的值有意义

一些参考文档:

在检查两个数组中指定范围的字节是否相等的方法rangeEquals便使用到了get方法。

/**
* checks equality for a specified range of bytes in two arrays
* 
* @param rangeStart the start position of the comparison range in this array
* @param compareTo the other array to compare
* @param cmpStart the start position of the comparison range in the other array
* @param length the length of the comparison range
* @return
*/
public boolean rangeEquals(long rangeStart, SegmentedByteArray compareTo, long cmpStart, int length) {
    for (int i = 0; i < length; i++)
        if (get(rangeStart + i) != compareTo.get(cmpStart + i))
            return false;
    return true;
}

loadFrom

loadFrom实现了从输入流HollowBlobInput读取byte到segments的逻辑。大体流程如下:

  1. 计算当前segment的大小,并初始化一个临时的byte数组scratch
  2. 根据需要加载的length长度,不断的循环从流读数据。
    • 每次读取的流大小为segmengSize和length较小的值。
  3. 调用HollowBlobInputread方法,读取数据到scratch,并计算整个scratch的长度,以便于后续继续读取数据。
  4. 按照一定的顺序将临时的scratch数组复制到segments中。
    • 这里可以认为是一种流式数据加载方式,按需分配内存,可以有效减少一次性加载文件带来的内存占用过大的问题。
@Override
public void loadFrom(HollowBlobInput is, long length) throws IOException {
    int segmentSize = 1 << log2OfSegmentSize;
    int segment = 0;

    byte scratch[] = new byte[segmentSize];

    while (length > 0) {
        ensureCapacity(segment);
        long bytesToCopy = Math.min(segmentSize, length);
        long bytesCopied = 0;
        while (bytesCopied < bytesToCopy) {
            bytesCopied += is.read(scratch, (int)bytesCopied, (int)(bytesToCopy - bytesCopied));
        }
        orderedCopy(scratch, 0, segments[segment++], 0, (int)bytesCopied);
        length -= bytesCopied;
    }
}

HollowBlobInputread方法会基于HollowBlobInput的不同实现使用不同的read实现。如下:

/**
* Reads up to {@code len} bytes of data from the HollowBlobInput by relaying the call to the underlying
* {@code DataInputStream} or {@code RandomAccessFile} into an array of bytes. This method blocks until at
* least one byte of input is available.
*
* @return an integer in the range 0 to 255
* @throws IOException if underlying {@code DataInputStream} or {@code RandomAccessFile}
* @throws UnsupportedOperationException if the input type wasn't  one of {@code DataInputStream} or {@code RandomAccessFile}
*/
public int read(byte b[], int off, int len) throws IOException {
    if (input instanceof RandomAccessFile) {
        return ((RandomAccessFile) input).read(b, off, len);
    } else if (input instanceof DataInputStream) {
        return ((DataInputStream) input).read(b, off, len);
    } else {
        throw new UnsupportedOperationException("Unknown Hollow Blob Input type");
    }
}

writeTo

loadFrom方法可以认为是数据的加载,即Consumer从输入流读取数据。writeTo方法可以认为是数据的写入,即Producer将生产数据写入到输出流。

写入的逻辑相对简单,大体上是首先计算需要写入的segmentSize,以及初始化剩余的byte位,不断按照每个segment的大小将数据写入到输出流,只需要全部segmentSize大小的数据写完。

/**
* Write a portion of this data to an OutputStream.
*
* @param os the output stream to write to
* @param startPosition the position to begin copying from this array
* @param len the length of the data to copy
* @throws IOException if the write to the output stream could not be performed
*/
public void writeTo(OutputStream os, long startPosition, long len) throws IOException {
    int segmentSize = 1 << log2OfSegmentSize;
    int remainingBytesInSegment = segmentSize - (int)(startPosition & bitmask);
    long remainingBytesInCopy = len;

    while (remainingBytesInCopy > 0) {
        long bytesToCopyFromSegment = Math.min(remainingBytesInSegment, remainingBytesInCopy);

        os.write(segments[(int)(startPosition >>> log2OfSegmentSize)], (int)(startPosition & bitmask), (int)bytesToCopyFromSegment);

        startPosition += bytesToCopyFromSegment;
        remainingBytesInSegment = segmentSize - (int)(startPosition & bitmask);
        remainingBytesInCopy -= bytesToCopyFromSegment;
    }
}

SegmentedByteArray使用Java原生的OutputStreamwrite方法写入流。

/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this output stream.
* The general contract for <code>write(b, off, len)</code> is that
* some of the bytes in the array <code>b</code> are written to the
* output stream in order; element <code>b[off]</code> is the first
* byte written and <code>b[off+len-1]</code> is the last byte written
* by this operation.
* <p>
* The <code>write</code> method of <code>OutputStream</code> calls
* the write method of one argument on each of the bytes to be
* written out. Subclasses are encouraged to override this method and
* provide a more efficient implementation.
* <p>
* If <code>b</code> is <code>null</code>, a
* <code>NullPointerException</code> is thrown.
* <p>
* If <code>off</code> is negative, or <code>len</code> is negative, or
* <code>off+len</code> is greater than the length of the array
* {@code b}, then an {@code IndexOutOfBoundsException} is thrown.
*
* @param      b     the data.
* @param      off   the start offset in the data.
* @param      len   the number of bytes to write.
* @exception  IOException  if an I/O error occurs. In particular,
*             an <code>IOException</code> is thrown if the output
*             stream is closed.
*/
public void write(byte b[], int off, int len) throws IOException {
    Objects.checkFromIndexSize(off, len, b.length);
    // len == 0 condition implicitly handled by loop bounds
    for (int i = 0 ; i < len ; i++) {
        write(b[off + i]);
    }
}

copy

SegmentedByteArray有两种copy的实现,一种是基于上文提到的set方法,从源数据中按照position读取数据到目标数组,支持任意ByteData实现类的copy操作,但是性能并不是最优的。相对简单,这里不过多赘述。

@Override
public void copy(ByteData src, long srcPos, long destPos, long length) {
    for (long i = 0; i < length; i++) {
        set(destPos++, src.get(srcPos++));
    }
}

第二种copy方法,针对SegmentedByteArray做了优化,相比通用的copy是速度更快的一种复制实现。那么此方法的速度快在哪里呢?主要包括以下几点:

  1. 所有的计算都使用位运算,并没有使用类似于++fori的循环处理
  2. 使用System.arraycopy赋值数组,而不是直接一位一位的计算segment
/**
* For a SegmentedByteArray, this is a faster copy implementation.
*
* @param src the source data
* @param srcPos the position to begin copying from the source data
* @param destPos the position to begin writing in this array
* @param length the length of the data to copy
*/
public void copy(SegmentedByteArray src, long srcPos, long destPos, long length) {
    int segmentLength = 1 << log2OfSegmentSize;
    int currentSegment = (int)(destPos >>> log2OfSegmentSize);
    int segmentStartPos = (int)(destPos & bitmask);
    int remainingBytesInSegment = segmentLength - segmentStartPos;

    while(length > 0) {
        int bytesToCopyFromSegment = (int) Math.min(remainingBytesInSegment, length);
        ensureCapacity(currentSegment);
        int copiedBytes = src.copy(srcPos, segments[currentSegment], segmentStartPos, bytesToCopyFromSegment);

        srcPos += copiedBytes;
        length -= copiedBytes;
        segmentStartPos = 0;
        remainingBytesInSegment = segmentLength;
        currentSegment++;
    }
}

/**
* copies exactly data.length bytes from this SegmentedByteArray into the provided byte array
*
* @param srcPos the position to begin copying from the source data
* @param data the source data
* @param destPos the position to begin writing in this array
* @param length the length of the data to copy
* @return the number of bytes copied
*/
public int copy(long srcPos, byte[] data, int destPos, int length) {
    int segmentSize = 1 << log2OfSegmentSize;
    int remainingBytesInSegment = (int)(segmentSize - (srcPos & bitmask));
    int dataPosition = destPos;

    while(length > 0) {
        byte[] segment = segments[(int)(srcPos >>> log2OfSegmentSize)];

        int bytesToCopyFromSegment = Math.min(remainingBytesInSegment, length);

        System.arraycopy(segment, (int)(srcPos & bitmask), data, dataPosition, bytesToCopyFromSegment);

        dataPosition += bytesToCopyFromSegment;
        srcPos += bytesToCopyFromSegment;
        remainingBytesInSegment = segmentSize - (int)(srcPos & bitmask);
        length -= bytesToCopyFromSegment;
    }

    return dataPosition - destPos;
}

orderedCopy

本小节分析orderedCopyorderedCopy 将此 SegmentedByteArray 中的 data.length 字节精确地复制到提供的字节数组中,以保证如果另一个线程看到更新,则该调用之前的所有其他写入对该线程也是可见的。

相比copy方法,orderedCopy方法最大的区别是保证了赋值操作的原子性。因为在赋值时,使用了sun.misc.Unsafe保证了put操作的原子性。

@Override
public void orderedCopy(VariableLengthData src, long srcPos, long destPos, long length) {
    int segmentLength = 1 << log2OfSegmentSize;
    int currentSegment = (int)(destPos >>> log2OfSegmentSize);
    int segmentStartPos = (int)(destPos & bitmask);
    int remainingBytesInSegment = segmentLength - segmentStartPos;

    while(length > 0) {
        int bytesToCopyFromSegment = (int) Math.min(remainingBytesInSegment, length);
        ensureCapacity(currentSegment);
        int copiedBytes = ((SegmentedByteArray) src).orderedCopy(srcPos, segments[currentSegment], segmentStartPos, bytesToCopyFromSegment);

        srcPos += copiedBytes;
        length -= copiedBytes;
        segmentStartPos = 0;
        remainingBytesInSegment = segmentLength;
        currentSegment++;
    }
}

/**
* copies exactly data.length bytes from this SegmentedByteArray into the provided byte array,
* guaranteeing that if the update is seen by another thread, then all other writes prior to
* this call are also visible to that thread.
*
* @param srcPos the position to begin copying from the source data
* @param data the source data
* @param destPos the position to begin writing in this array
* @param length the length of the data to copy
* @return the number of bytes copied
*/
private int orderedCopy(long srcPos, byte[] data, int destPos, int length) {
    int segmentSize = 1 << log2OfSegmentSize;
    int remainingBytesInSegment = (int)(segmentSize - (srcPos & bitmask));
    int dataPosition = destPos;

    while(length > 0) {
        byte[] segment = segments[(int)(srcPos >>> log2OfSegmentSize)];

        int bytesToCopyFromSegment = Math.min(remainingBytesInSegment, length);

        orderedCopy(segment, (int)(srcPos & bitmask), data, dataPosition, bytesToCopyFromSegment);

        dataPosition += bytesToCopyFromSegment;
        srcPos += bytesToCopyFromSegment;
        remainingBytesInSegment = segmentSize - (int)(srcPos & bitmask);
        length -= bytesToCopyFromSegment;
    }

    return dataPosition - destPos;
}

private void orderedCopy(byte[] src, int srcPos, byte[] dest, int destPos, int length) {
    int endSrcPos = srcPos + length;
    destPos += Unsafe.ARRAY_BYTE_BASE_OFFSET;

    while (srcPos < endSrcPos) {
        unsafe.putByteVolatile(dest, destPos++, src[srcPos++]);
    }
}

HollowUnsafeHandle

那么sun.misc.Unsafe就是何方神圣呢?

简单讲一下这个类。Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。

如果对优秀开源项目有过深入分析研究的同学应该对sun.misc.Unsafe并不陌生,为了追求极致性能,绝大部分的高性能框架或项目基本上都离不开sun.misc.Unsafe类,Unsafe并不是说这个类中的方法不安全,而是说想要使用好sun.misc.Unsafe并不容易,需要对sun.misc.Unsafe和底层原理有深刻的理解,因此并不推荐在一般项目中直接使用,这也从一个侧面说明了为何Unsafe类并不是在java包下,而是在sun包下。

这个类尽管里面的方法都是public的,但是并没有办法使用它们,JDK API文档也没有提供任何关于这个类的方法的解释。总而言之,对于Unsafe类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然JDK库里面的类是可以随意使用的。

Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。不过Java本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。正因为Unsafe的硬件级别的操作,使得Unsafe的性能极高,在追求高性能的场景下使用极为广泛。

HollowUnsafeHandle的实现比较简单,就是通过theUnsafe方法来获取一个Unsafe的静态对象。

package com.netflix.hollow.core.memory;

import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import sun.misc.Unsafe;

@SuppressWarnings("restriction")
public class HollowUnsafeHandle {
    private static final Logger log = Logger.getLogger(HollowUnsafeHandle.class.getName());
    private static final Unsafe unsafe;

    static {
        Field theUnsafe;
        Unsafe u = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            u = (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            log.log(Level.SEVERE, "Unsafe access failed", e);
        }
        unsafe = u;
    }

    public static Unsafe getUnsafe() {
        return unsafe;
    }
}

用到的一些UnSafe的方法:

@ForceInline
public void putByteVolatile(Object o, long offset, byte x) {
    theInternalUnsafe.putByteVolatile(o, offset, x);
}
/** Volatile version of {@link #putByte(Object, long, byte)}  */
@HotSpotIntrinsicCandidate
public native void  putByteVolatile(Object o, long offset, byte x);

destroy

SegmentedByteArray之所以有destroy方法,是因为SegmentedByteArray可以单独开辟内存空间,也就byte的二维数组,hollow为了保证内存的充分利用,如果使用SegmentedByteArray存储数据,会要求当数据在不在使用时主动[销毁],但是这里的销毁并不是将这块内存内容改为null,而是将bytearray归还到回收站中,如果有新的数据需要申请内存,将直接从回收站总恢复即可。

public void destroy() {
    for (int i = 0; i < segments.length; i++) {
        if (segments[i] != null)
            memoryRecycler.recycleByteArray(segments[i]);
    }
}

size

SegmentedByteArray计算的size是整个二维数组的长度。与上文中提到的ArrayByteData中使用的一维数组有区别。

@Override
public long size() {
    long size = 0;
    for (int i=0; i< segments.length; i++) {
        if (segments[i] != null)
            size += segments[i].length;
    }

    return size;
}

VariableLengthDataFactory

VariableLengthData包含EncodedByteBufferSegmentedByteArray两种实现方式,各有各的特点。

  1. EncodedByteBuffer使用BlobByteBuff作为数据存储,即一个列表指针数组,对应的MomoryMode为SHARED_MEMORY_LAZY
  2. SegmentedByteArray自己维护了一个二维数组,对应的MomoryMode为ON_HEAP

基于此,在创建VariableLengthData时,也就有不同的工厂方法,如下VariableLengthDataFactory的实现。

public class VariableLengthDataFactory {

    private static final Logger LOG = Logger.getLogger(VariableLengthDataFactory.class.getName());

    public static VariableLengthData get(MemoryMode memoryMode, ArraySegmentRecycler memoryRecycler) {

        if (memoryMode.equals(MemoryMode.ON_HEAP)) {
            return new SegmentedByteArray(memoryRecycler);

        } else if (memoryMode.equals(MemoryMode.SHARED_MEMORY_LAZY)) {
            /// list pointer array
            return new EncodedByteBuffer();
        } else {
            throw new UnsupportedOperationException("Memory mode " + memoryMode.name() + " not supported");
        }
    }

    public static void destroy(VariableLengthData vld) {
        if (vld instanceof SegmentedByteArray) {
            ((SegmentedByteArray) vld).destroy();
        } else if (vld instanceof EncodedByteBuffer) {
            LOG.warning("Destroy operation is a no-op in shared memory mode");
        } else {
            throw new UnsupportedOperationException("Unknown type");
        }
    }
}

GapEncodedVariableLengthIntegerReader

GapEncodedVariableLengthIntegerReader的名字很长,但是理解起来并不是很复杂。首先来看一个应用场景:

void readFromInput(HollowBlobInput in, boolean isDelta, HollowObjectSchema unfilteredSchema) throws IOException {
    maxOrdinal = VarInt.readVInt(in);

    if (isDelta) {
        encodedRemovals = GapEncodedVariableLengthIntegerReader.readEncodedDeltaOrdinals(in, memoryRecycler);
        encodedAdditions = GapEncodedVariableLengthIntegerReader.readEncodedDeltaOrdinals(in, memoryRecycler);
    }

    readFieldStatistics(in, unfilteredSchema);

    fixedLengthData = FixedLengthDataFactory.get(in, memoryMode, memoryRecycler);
    removeExcludedFieldsFromFixedLengthData();

    readVarLengthData(in, unfilteredSchema);
}

public static GapEncodedVariableLengthIntegerReader readEncodedDeltaOrdinals(HollowBlobInput in, ArraySegmentRecycler memoryRecycler) throws IOException {
    SegmentedByteArray arr = new SegmentedByteArray(memoryRecycler);

    long numBytesEncodedOrdinals = VarInt.readVLong(in);

    arr.loadFrom(in, numBytesEncodedOrdinals);

    return new GapEncodedVariableLengthIntegerReader(arr, (int)numBytesEncodedOrdinals);
}

通过上文的应用场景可以看出来,GapEncodedVariableLengthIntegerReader主要封装了读取delta数据后,如何将delta数据合并到已有全量数据的功能,这样就解释了为什么会命名为gap。下文将逐一分析具体的方法。

注:GapEncodedVariableLengthIntegerReader是基于链表的数据结构设计和实现的。

属性和构造方法

首先我们来看下GapEncodedVariableLengthIntegerReader的属性和方法。

public static GapEncodedVariableLengthIntegerReader EMPTY_READER = new GapEncodedVariableLengthIntegerReader(null, 0) {
    @Override
    public int nextElement() {
        return Integer.MAX_VALUE;
    }
};

// 空歌白石:已有全量数据的引用,这里使用的SegmentedByteArray实现。
private final SegmentedByteArray data;
// 空歌白石:待合并的字节数
private final int numBytes;
// 空歌白石:当前合并的position,可以作为上文中ByteData的readLongBits等方法的参数
private int currentPosition;

// 空歌白石:待读取的下一个元素
private int nextElement;
// 空歌白石:元素对应的索引,这里的index,可以作为上文中ByteData的get方法的参数
private int elementIndex;

public GapEncodedVariableLengthIntegerReader(SegmentedByteArray data, int numBytes) {
    this.data = data;
    this.numBytes = numBytes;
    reset();
}

在属性中有一个EMPTY_READER,可以任务是定义了一个初始化的reader,可以作为任意可变长度读取器的初始化属性。以下是两个初始化的例子,通过三元运算符计算后如果结果为null,则使用EMPTY_READER进行初始化。

removalsReader = from.encodedRemovals == null ? GapEncodedVariableLengthIntegerReader.EMPTY_READER : from.encodedRemovals;

GapEncodedVariableLengthIntegerReader oldRemovals = currentData.encodedRemovals == null ? GapEncodedVariableLengthIntegerReader.EMPTY_READER : currentData.encodedRemovals;

reset & advance

上文的构造方法中调用了reset方法,那么reset方法又是做什么的呢?实际上就是将position、index、element等属性重置。重置后,会调用advance方法,尝试向前读取下一个元素,但是由于已经被重置过,所以并不会影响实际的position等值。

但是有一个需要特别注意的地方,比如以上文中EMPTY_READER来讲,初始化时numBytescurrentPosition都是0,那么会将nextElement赋值为Integer.MAX_VALUE,这也是为什么reset方法要调用看似没必要的advance的原因。

public void reset() {
    currentPosition = 0;
    elementIndex = -1;
    nextElement = 0;
    advance();
}

advance方法就是根据currentPosition计算出下一个需要读取元素的Position,并将调整向后移动指针。

public void advance() {
    if(currentPosition == numBytes) {
        nextElement = Integer.MAX_VALUE;
    } else {
        int nextElementDelta = VarInt.readVInt(data, currentPosition);
        currentPosition += VarInt.sizeOfVInt(nextElementDelta);
        nextElement += nextElementDelta;
        elementIndex++;
    }
}

element

GapEncodedVariableLengthIntegerReader本身实现了一个链表,因此是通过nextElement()方法获取下一个元素。在某些需要复制的场景需要计算出剩余的元素数量,会使用到remainingElements()方法。

public int nextElement() {
    return nextElement;
}

public int remainingElements() {
    int remainingElementCount = 0;
    while(nextElement != Integer.MAX_VALUE) {
        remainingElementCount++;
        advance();
    }
    return remainingElementCount;
}

writeTo

GapEncodedVariableLengthIntegerReader的写入操作基于SegmentedByteArraywriteTo方法实现,这里不再赘述,关于VarInt.writeVInt是如何实现的,将在下文VarInt小节专门讲解。

public void writeTo(OutputStream os) throws IOException {
    VarInt.writeVInt(os, numBytes);
    data.writeTo(os, 0, numBytes);
}

destroy

GapEncodedVariableLengthIntegerReader的销毁操作基于SegmentedByteArraydestroy方法实现,这里不再赘述。

public void destroy() {
    if(data != null)
        data.destroy();
}

delta ordinal process

本小节主要讲解GapEncodedVariableLengthIntegerReader对于DeltaOrdinal的三个操作方法,

read

readEncodedDeltaOrdinals实现了从输入流读取数据到GapEncodedVariableLengthIntegerReader的功能,最终数据会存储在SegmentedByteArray的二维字节数组内。

public static GapEncodedVariableLengthIntegerReader readEncodedDeltaOrdinals(HollowBlobInput in, ArraySegmentRecycler memoryRecycler) throws IOException {
    SegmentedByteArray arr = new SegmentedByteArray(memoryRecycler);

    long numBytesEncodedOrdinals = VarInt.readVLong(in);

    arr.loadFrom(in, numBytesEncodedOrdinals);

    return new GapEncodedVariableLengthIntegerReader(arr, (int)numBytesEncodedOrdinals);
}

copy

copyEncodedDeltaOrdinals实现了将一个输入流,赋值到1到n个输出流的逻辑,这个copy方法很有用,复制后的数据Consumer可以进行更深层次的加工,而不需要重新读取输入流。

public static void copyEncodedDeltaOrdinals(HollowBlobInput in, DataOutputStream... os) throws IOException {
    long numBytesEncodedOrdinals = IOUtils.copyVLong(in, os);
    IOUtils.copyBytes(in, os, numBytesEncodedOrdinals);
}

discard

discardEncodedDeltaOrdinals丢弃或者忽略部分字节流。主要用于当delta中有需要删除的数据的场景。

public static void discardEncodedDeltaOrdinals(HollowBlobInput in) throws IOException {
    long numBytesToSkip = VarInt.readVLong(in);
    while(numBytesToSkip > 0) {
        numBytesToSkip -= in.skipBytes(numBytesToSkip);
    }
}

combine

combine操作是GapEncodedVariableLengthIntegerReader的一个核心方法,也是Hollow提供数据的合并核心逻辑。但是实现的逻辑并不是很复杂。

  1. 手下将两个reader进行reset。
  2. 基于传入的内存回收站,重新申请内存空间,比较两个元素的值
  3. 最终合并到一个Array中。

上述的逻辑可以通过以下图形表示:

Hollow-GapEncodedVariableLengthIntegerReader-combine.drawio.png

public static GapEncodedVariableLengthIntegerReader combine(GapEncodedVariableLengthIntegerReader reader1, GapEncodedVariableLengthIntegerReader reader2, ArraySegmentRecycler memoryRecycler) {
    reader1.reset();
    reader2.reset();
    ByteDataArray arr = new ByteDataArray(memoryRecycler);
    int cur = 0;

    while(reader1.nextElement() != Integer.MAX_VALUE || reader2.nextElement() != Integer.MAX_VALUE) {
        if(reader1.nextElement() < reader2.nextElement()) {
            VarInt.writeVInt(arr, reader1.nextElement() - cur);
            cur = reader1.nextElement();
            reader1.advance();
        } else if(reader2.nextElement() < reader1.nextElement()) {
            VarInt.writeVInt(arr, reader2.nextElement() - cur);
            cur = reader2.nextElement();
            reader2.advance();
        } else {
            VarInt.writeVInt(arr, reader1.nextElement() - cur);
            cur = reader1.nextElement();
            reader1.advance();
            reader2.advance();
        }
    }

    return new GapEncodedVariableLengthIntegerReader(arr.getUnderlyingArray(), (int)arr.length());
}

合并方法中用到的ByteDataArray将在下文中讲解。

ArraySegmentRecycler

ArraySegmentRecycler 可以认为是一个内存池。Hollow通过池化方案以及重用内存,以达到在更新数据时最大限度地减少GC影响。

ArraySegmentRecycler的内存池保存在堆上的数组。池中的每个数组都有固定的长度。当 Hollow 中需要长数组或字节数组时,它会将池化数组段拼接为 SegmentedByteArraySegmentedLongArray,这些类封装了将分段数组视为连续值范围的细节。

ArraySegmentRecycler.png

public interface ArraySegmentRecycler {

    // 空歌白石:处理回收SegmentedLongArray的方法
    public int getLog2OfLongSegmentSize();
    public long[] getLongArray();
    public void recycleLongArray(long[] arr);

    // 空歌白石:处理回收SegmentedByteArray的方法
    public int getLog2OfByteSegmentSize();
    public byte[] getByteArray();
    public void recycleByteArray(byte[] arr);

    // 空歌白石:空间交换方法,不同的回收站实现有不同的逻辑,下文将详细讲解。
    public void swap();
}

个人以为ArraySegmentRecycler接口的设计可以更优雅一些,其中一个典型的问题就是ArraySegmentRecyclerlongArraybyteArray的操作方法混合在一起。

RecyclingRecycler

RecyclingRecycler 是一种ArraySegmentRecycler的实现,它将byte数组池化,做到内存的可回收利用,避免了内存的不断膨胀。

构造方法和属性

RecyclingRecycler定义了关于long和byte的recyclersegmentSize。其中的核心是Recycler,我们将在下文中详细讲解。

private final int log2OfByteSegmentSize;
private final int log2OfLongSegmentSize;
private final Recycler<long[]> longSegmentRecycler;
private final Recycler<byte[]> byteSegmentRecycler;

public RecyclingRecycler() {
    this(11, 8);
}

public RecyclingRecycler(final int log2ByteArraySize, final int log2LongArraySize) {
    this.log2OfByteSegmentSize = log2ByteArraySize;
    this.log2OfLongSegmentSize = log2LongArraySize;

    byteSegmentRecycler = new Recycler<>(() -> new byte[1 << log2ByteArraySize]);
    // Allocated size is increased by 1, see JavaDoc of FixedLengthElementArray for details
    longSegmentRecycler = new Recycler<>(() -> new long[(1 << log2LongArraySize) + 1]);
}

LongArray

LongArraylongSegmentRecycler的回收器中获取内存空间,响应的回收时,也是将long数组放回longSegmentRecycler

public int getLog2OfLongSegmentSize() {
    return log2OfLongSegmentSize;
}

public long[] getLongArray() {
    long[] arr = longSegmentRecycler.get();
    Arrays.fill(arr, 0);
    return arr;
}

public void recycleLongArray(long[] arr) {
    longSegmentRecycler.recycle(arr);
}

在获取long数组时,使用了Arrays.fill(arr, 0);方法从回收站得到的新的内存空间,并将整个arr的全部元素赋值为0。

/**
* Assigns the specified long value to each element of the specified array
* of longs.
*
* @param a the array to be filled
* @param val the value to be stored in all elements of the array
*/
public static void fill(long[] a, long val) {
    for (int i = 0, len = a.length; i < len; i++)
        a[i] = val;
}

ByteArray

ByteArraybyteSegmentRecycler的回收器中获取内存空间,响应的回收时,也是将byte数组放回byteSegmentRecycler

public int getLog2OfByteSegmentSize() {
    return log2OfByteSegmentSize;
}

public byte[] getByteArray() {
    // @@@ should the array be filled?
    return byteSegmentRecycler.get();
}

public void recycleByteArray(byte[] arr) {
    byteSegmentRecycler.recycle(arr);
}

swap

依赖于Recyclerswap方法。

public void swap() {
    longSegmentRecycler.swap();
    byteSegmentRecycler.swap();
}

Recycler

RecyclerRecyclingRecycler的内部类。负责具体的内存回收再利用工作。

构造方法和属性

Recycler内部维护了两个队列,一个是用于分配的地址,一个是用于接收回收的队列。使用队列能够避免内存混乱,分配和收集内部节点。

private final Creator<T> creator;
private Deque<T> currentSegments;
private Deque<T> nextSegments;

Recycler(Creator<T> creator) {
    // Use an ArrayDeque instead of a LinkedList
    // This will avoid memory churn allocating and collecting internal nodes
    this.currentSegments = new ArrayDeque<>();
    this.nextSegments = new ArrayDeque<>();
    this.creator = creator;
}

get

如果回收站中由可分配的内存段,那么从缓存中取出,如果没有的话,重新创建。

T get() {
    if (!currentSegments.isEmpty()) {
        return currentSegments.removeFirst();
    }

    return creator.create();
}

recycle

回收的方法就是将放入回收站的segment放回到nextsegment中。

void recycle(T reuse) {
    nextSegments.addLast(reuse);
}

swap

Recycler维护了两个队列,当备份队列的大小大于当前队列时,就会触发地址交换逻辑。将预分配的队列添加到当前的队列中。并清空待回收的队列。

void swap() {
    // Swap the deque references to reduce addition and clearing cost
    if (nextSegments.size() > currentSegments.size()) {
        Deque<T> tmp = nextSegments;
        nextSegments = currentSegments;
        currentSegments = tmp;
    }

    currentSegments.addAll(nextSegments);
    nextSegments.clear();
}

Creator

CreatorRecycler的内部使用的接口类。Creator仅有一个create方法。可以任意实现需要创建的内存对象。

private interface Creator<T> {
    T create();
}

比如:byteSegmentRecycler的实现为:() -> new byte[1 << log2ByteArraySize]

public RecyclingRecycler(final int log2ByteArraySize, final int log2LongArraySize) {
    this.log2OfByteSegmentSize = log2ByteArraySize;
    this.log2OfLongSegmentSize = log2LongArraySize;

    byteSegmentRecycler = new Recycler<>(() -> new byte[1 << log2ByteArraySize]);
    // Allocated size is increased by 1, see JavaDoc of FixedLengthElementArray for details
    longSegmentRecycler = new Recycler<>(() -> new long[(1 << log2LongArraySize) + 1]);
}

总结

这里我们用一张图来表示RecyclingRecycler的实现逻辑

Hollow-RecyclingRecycler.drawio.png

WastefulRecycler

WastefulRecycler 是另外一种ArraySegmentRecycler的实现,从Wasteful[adj.浪费的,挥霍的]可以看出,此种实现并没有真正池化数组,而是根据需要创建它们,会肆意的挥霍内存空间。

从源码也可以印证上述观点,getArray方法中,并不会像RecyclingRecycler一样重复的利用已有的内存空间,而是每次需要Array空间时,都重新申请内存。

WastefulRecycler定义了两个静态变量DEFAULT_INSTANCESMALL_ARRAY_RECYCLER,没别指定量不同的byte和long的segment大小,可以按需使用。其中DEFAULT_INSTANCE的空间分配和RecyclingRecycler的默认分配是一致的。

public class WastefulRecycler implements ArraySegmentRecycler {

    public static WastefulRecycler DEFAULT_INSTANCE = new WastefulRecycler(11, 8);
    public static WastefulRecycler SMALL_ARRAY_RECYCLER = new WastefulRecycler(5, 2);

    private final int log2OfByteSegmentSize;
    private final int log2OfLongSegmentSize;

    public WastefulRecycler(int log2OfByteSegmentSize, int log2OfLongSegmentSize) {
        this.log2OfByteSegmentSize = log2OfByteSegmentSize;
        this.log2OfLongSegmentSize = log2OfLongSegmentSize;
    }

    @Override
    public int getLog2OfByteSegmentSize() {
        return log2OfByteSegmentSize;
    }

    @Override
    public int getLog2OfLongSegmentSize() {
        return log2OfLongSegmentSize;
    }

    @Override
    public long[] getLongArray() {
        return new long[(1 << log2OfLongSegmentSize) + 1];
    }

    @Override
    public byte[] getByteArray() {
        return new byte[(1 << log2OfByteSegmentSize)];
    }

    @Override
    public void recycleLongArray(long[] arr) {
        /// do nothing
    }

    @Override
    public void recycleByteArray(byte[] arr) {
        /// do nothing
    }

    @Override
    public void swap() {
        // do nothing
    }
}

FixedLengthData

Hollow 中的每条记录都以固定长度的位数开始。在最底层,这些数据保存在 FixedLengthData 数据结构中,可以由长数组或 ByteBuffers 支持。

例如,如果查询 EncodedLongBuffer 以获取以下示例位范围中从第7位开始的6位区间: 0001000100100001101000010100101001111010101010010010101 将返回二进制值 100100 或以十进制的36

因此,有两种方法可以从给定位索引的位串中获取元素值。

  • 第一种,对长度小于 59 位的值使用 getElementValue
  • 第二种,对长度不超过 64 位的值使用推荐的 getLargeElementValue
public interface FixedLengthData {

    // 空歌白石:获取给定位index处的元素值,由bitsPerElement位组成。bitsPerElement应小于 59 位。
    long getElementValue(long index, int bitsPerElement);

    // 空歌白石:获取给定位index处的元素值,由bitsPerElement位组成。bitsPerElement应小于 59 位。
    // 空歌白石:mask,在返回之前应用于元素值的掩码。 掩码应小于或等于 (1L << bitsPerElement) - 1 以保证在所需元素值之前和之后出现的一个或多个(可能)部分元素值不包含在返回值中。
    long getElementValue(long index, int bitsPerElement, long mask);

    // 空歌白石:获取给定位索引处的大元素值,由 bitsPerElement 位组成。如果 bitsPerElement 可能超过 58 位,则应使用此方法,否则可以使用 getLargeElementValue(long, int) 方法。
    long getLargeElementValue(long index, int bitsPerElement);

    // 空歌白石:获取给定位索引处的大元素值,由 bitsPerElement 位组成。如果 bitsPerElement 可能超过 58 位,则应使用此方法,否则可以使用 getLargeElementValue(long, int) 方法。
    // 空歌白石:在返回之前应用于元素值的掩码。 掩码应小于或等于 (1L << bitsPerElement) - 1 以保证在所需元素值之前和之后出现的一个或多个(可能)部分元素值不包含在返回值中。
    long getLargeElementValue(long index, int bitsPerElement, long mask);

    // 空歌白石:在给定index赋值
    void setElementValue(long index, int bitsPerElement, long value);

    // 空歌白石:赋值数据到指定的位置
    void copyBits(FixedLengthData copyFrom, long sourceStartBit, long destStartBit, long numBits);

    // 空歌白石:扩容操作
    void incrementMany(long startBit, long increment, long bitsBetweenIncrements, int numIncrements);

    // 空歌白石:清空对应位置的数据
    void clearElementValue(long index, int bitsPerElement);

    // 空歌白石:从输入中丢弃固定长度的数据。输入包含要丢弃的 long 的数量。
    static void discardFrom(HollowBlobInput in) throws IOException {
        long numLongs = VarInt.readVLong(in);
        long bytesToSkip = numLongs * 8;

        while(bytesToSkip > 0) {
            bytesToSkip -= in.skipBytes(bytesToSkip);
        }
    }

    // 计算value值所需的位数
    static int bitsRequiredToRepresentValue(long value) {
        if(value == 0)
            return 1;
        return 64 - Long.numberOfLeadingZeros(value);
    }

}

FixedLengthData的继承关系如下图:

FixedLengthData.png

EncodedLongBuffer

EncodedLongBuffer基于BlobByteBuffer实现数据的存储,使用的是共享的内存空间。是对FixedLengthData的第一种实现方式。

EncodedLongBuffer允许在 ByteBuffers 中存储和检索固定长度的数据。EncodedLongBuffer的核心存储结构为如下,与BlobByteBuffer存储结构一致。

Hollow-BlobByteBuffer.drawio.png

有两种方法可以从给定位索引的位串中获取元素值。

  1. getElementValue(long, int)

  2. getElementValue(long, int, long) 在缓冲区内的字节索引偏移处。

  3. getLargeElementValue(long, int)

  4. getLargeElementValue(long, int, long):

通过读取两个 long 值,然后从覆盖这两个值的位组成一个元素值。

在对应的 FixedLengthElementArray 实现中,对最后 8 个字节数据的长读取是安全的,因为最后填充了 1 个长。相反,如果查询超过缓冲区容量的 8 字节范围,则此实现返回零字节。

getElementValue 只能支持 58 位或更少的元素值。这是因为读取与字节边界不对齐的值需要在一个字节内移动地址偏移的位数。对于 58 位值,与字节边界的偏移量可高达 6 位。 58 位可以移动 6 位,并且仍然适合 64 位空间。对于 59 位值,与字节边界的偏移量可高达 7 位。将 59 位值移位 6 位或 7 位都会溢出 64 位空间,导致读取时值无效。

构造方法和属性

EncodedLongBuffer的构造方法相比前文分析类简单的太多。

// 空歌白石:ByteBuffer的视图
private BlobByteBuffer bufferView;

// 空歌白石:可存储的最大字节索引
private long maxByteIndex = -1;

public EncodedLongBuffer() {}

newFrom & loadFrom

newFrom是对外部开放的方法,loadFrom是一个私有方法,newFrom可以基于loadFromHollowBlobInput转换为EncodedLongBuffer。并将HollowBlobInput的buffer写入到EncodedLongBufferbufferView中。

/**
* Returns a new EncodedLongBuffer from deserializing the given input. The value of the first variable length integer
* in the input indicates how many long values are to then be read from the input.
*
* @param in Hollow Blob Input to read data (a var int and then that many longs) from
* @return new EncodedLongBuffer containing data read from input
*/
public static EncodedLongBuffer newFrom(HollowBlobInput in) throws IOException {
    long numLongs = VarInt.readVLong(in);

    return newFrom(in, numLongs);
}

/**
* Returns a new EncodedLongBuffer from deserializing numLongs longs from given input.
*
* @param in Hollow Blob Input to read numLongs longs from
* @return new EncodedLongBuffer containing data read from input
*/
public static EncodedLongBuffer newFrom(HollowBlobInput in, long numLongs) throws IOException {
    EncodedLongBuffer buf = new EncodedLongBuffer();

    buf.loadFrom(in, numLongs);

    return buf;
}

private void loadFrom(HollowBlobInput in, long numLongs) throws IOException {
    BlobByteBuffer buffer = in.getBuffer();

    if(numLongs == 0)
        return;

    this.maxByteIndex = (numLongs * Long.BYTES) - 1;

    buffer.position(in.getFilePointer());

    this.bufferView = buffer.duplicate();

    buffer.position(buffer.position() + (numLongs * Long.BYTES));

    in.seek(in.getFilePointer() + (numLongs  * Long.BYTES));
}

EncodedLongBufferloadFrom基于RandomAccessFilegetFilePointer()方法读取文件的offset,从这点可以看出,如果想要使用EncodedLongBuffer缓存数据,只能基于文件在Producer和Consumer之间进行数据传输,不能只使用DataInputStream。这也限制了EncodedLongBuffer的作用,因此更推荐使用FixedLengthElementArray管理数据。

/**
* Returns the current offset in this input at which the next read would occur.
*
* @return current offset from the beginning of the file, in bytes
* @exception IOException if an I/O error occurs.
*/
public long getFilePointer() throws IOException {
    if (input instanceof RandomAccessFile) {
        return ((RandomAccessFile) input).getFilePointer();
    } else if (input instanceof DataInputStream) {
        throw new UnsupportedOperationException("Can not get file pointer for Hollow Blob Input of type DataInputStream");
    } else {
        throw new UnsupportedOperationException("Unknown Hollow Blob Input type");
    }
}

Long.BYTES的定义。

/**
* The number of bits used to represent a {@code long} value in two's
* complement binary form.
*
* 空歌白石:用于以二进制补码形式表示 long 值的字节数。
*
* @since 1.5
*/
@Native public static final int SIZE = 64;
/**
* The number of bytes used to represent a {@code long} value in two's
* complement binary form.
* 
* 空歌白石:用于以二进制补码形式表示 long 值的字节数。
*
* @since 1.8
*/
public static final int BYTES = SIZE / Byte.SIZE;

Byte.SIZE的定义。

/**
* The number of bits used to represent a {@code byte} value in two's
* complement binary form.
*
* 空歌白石:用于以二进制补码形式表示 byte 值的字节数。
*
* @since 1.5
*/
public static final int SIZE = 8;

/**
* The number of bytes used to represent a {@code byte} value in two's
* complement binary form.
*
* 空歌白石:用于以二进制补码形式表示 byte 值的字节数。
*
* @since 1.8
*/
public static final int BYTES = SIZE / Byte.SIZE;

getElementValue

getElementValue是在小于64位的长度情况下读取数据的方法,如果大于64位,需要使用getLargeElementValue方法。

@Override
public long getElementValue(long index, int bitsPerElement) {
    return getElementValue(index, bitsPerElement, ((1L << bitsPerElement) - 1));
}

@Override
public long getElementValue(long index, int bitsPerElement, long mask) {

    long whichByte = index >>> 3;
    int whichBit = (int) (index & 0x07);

    if (whichByte + ceil((float) bitsPerElement/8) > this.maxByteIndex + 1) {
        throw new IllegalStateException();
    }

    long longVal = this.bufferView.getLong(this.bufferView.position() + whichByte);
    long l =  longVal >>> whichBit;
    return l & mask;
}

getLargeElementValue

@Override
public long getLargeElementValue(long index, int bitsPerElement) {
    long mask = bitsPerElement == 64 ? -1 : ((1L << bitsPerElement) - 1);
    return getLargeElementValue(index, bitsPerElement, mask);
}

@Override
public long getLargeElementValue(long index, int bitsPerElement, long mask) {

    long whichLong = index >>> 6;
    int whichBit = (int) (index & 0x3F);

    long l = this.bufferView.getLong(bufferView.position() + whichLong * Long.BYTES) >>> whichBit;

    int bitsRemaining = 64 - whichBit;

    if (bitsRemaining < bitsPerElement) {
        whichLong++;
        l |= this.bufferView.getLong(bufferView.position() + whichLong * Long.BYTES) << bitsRemaining;
    }

    return l & mask;
}

SegmentedLongArray

在分析FixedLengthElementArray前,让我们先看下SegmentedLongArray的实现。

构造方法与属性

SegmentedLongArray使用Unsafe保证数据get、set、fill等操作的原子性。

private static final Unsafe unsafe = HollowUnsafeHandle.getUnsafe();

// 空歌白石:FixedLengthElementArray的堆占用。
protected final long[][] segments;

protected final int log2OfSegmentSize;
protected final int bitmask;

public SegmentedLongArray(ArraySegmentRecycler memoryRecycler, long numLongs) {
    this.log2OfSegmentSize = memoryRecycler.getLog2OfLongSegmentSize();

    // 空歌白石:计算需要的初始化segments。
    int numSegments = (int)((numLongs - 1) >>> log2OfSegmentSize) + 1;
    long[][] segments = new long[numSegments][];

    // 空歌白石:计算掩码。
    this.bitmask = (1 << log2OfSegmentSize) - 1;

    for (int i = 0; i < segments.length; i++) {
        // 空歌白石:从回收站中获取空闲的segment。
        segments[i] = memoryRecycler.getLongArray();
    }

    /// The following assignment is purposefully placed *after* the population of all segments.
    /// The final assignment after the initialization of the array guarantees that no thread
    /// will see any of the array elements before assignment.
    /// We can't risk the segment values being visible as null to any thread, because
    /// FixedLengthElementArray uses Unsafe to access these values, which would cause the
    /// JVM to crash with a segmentation fault.
    this.segments = segments;
}

set/get

getset方法可以通过SegmentedLongArray的存储结构图轻松理解。

Hollow-SegmentedLongArray.drawio.png

public void set(long index, long value) {
    int segmentIndex = (int)(index >> log2OfSegmentSize);
    int longInSegment = (int)(index & bitmask);
    unsafe.putLong(segments[segmentIndex], (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (8 * longInSegment), value);

    /// duplicate the longs here so that we can read faster.
    if(longInSegment == 0 && segmentIndex != 0) {
        unsafe.putLong(segments[segmentIndex - 1], (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (8 * (1 << log2OfSegmentSize)), value);
    }
}

public long get(long index) {
    int segmentIndex = (int)(index >>> log2OfSegmentSize);
    long ret = segments[segmentIndex][(int)(index & bitmask)];

    return ret;
}

fill

SegmentedLongArray按照Unsafe.ARRAY_LONG_BASE_OFFSET的偏移量定义第二维度的长度。

public void fill(long value) {
    for (int i = 0; i < segments.length; i++) {
        long offset = Unsafe.ARRAY_LONG_BASE_OFFSET;
        for (int j = 0; j < segments[i].length; j++) {
            unsafe.putLong(segments[i], offset, value);
            offset += 8;
        }
    }
}

Unsafe.ARRAY_LONG_BASE_OFFSET的定义如下,基于具体的class类型使用native类型方法计算偏移量。

/** The value of {@code arrayBaseOffset(long[].class)} */
public static final int ARRAY_LONG_BASE_OFFSET = jdk.internal.misc.Unsafe.ARRAY_LONG_BASE_OFFSET;

/** The value of {@code arrayBaseOffset(long[].class)} */
public static final int ARRAY_LONG_BASE_OFFSET = theUnsafe.arrayBaseOffset(long[].class);

/**
* Reports the offset of the first element in the storage allocation of a
* given array class.  If {@link #arrayIndexScale} returns a non-zero value
* for the same class, you may use that scale factor, together with this
* base offset, to form new offsets to access elements of arrays of the
* given class.
*
* @see #getInt(Object, long)
* @see #putInt(Object, long, int)
*/
public int arrayBaseOffset(Class<?> arrayClass) {
    if (arrayClass == null) {
        throw new NullPointerException();
    }

    return arrayBaseOffset0(arrayClass);
}

private native int arrayBaseOffset0(Class<?> arrayClass);

destroy

销毁方法是将不需要的segments数组放回回收站,以便于后续继续使用。

public void destroy(ArraySegmentRecycler memoryRecycler) {
    for(int i = 0;i < segments.length; i++) {
        if(segments[i] != null)
            memoryRecycler.recycleLongArray(segments[i]);
    }
}

writeTo

writeTo方法实现了将segments的数据写入到DataOutputStream的任务,具体输出流的长度需要首先计算出来。如何计算numLongs呢? 一般通过long numLongsRequired = numBitsRequired == 0 ? 0 : ((numBitsRequired - 1) / 64) + 1;方式得来,其中的numBitsRequired会在其他文章中ByteArrayOrdinalMap进行介绍。

public void writeTo(DataOutputStream dos, long numLongs) throws IOException {
    VarInt.writeVLong(dos, numLongs);

    for(long i = 0; i < numLongs; i++) {
        dos.writeLong(get(i));
    }
}

JDK的DataOutputStreamwriteLong实现:

/**
* Writes a <code>long</code> to the underlying output stream as eight
* bytes, high byte first. In no exception is thrown, the counter
* <code>written</code> is incremented by <code>8</code>.
*
* @param      v   a <code>long</code> to be written.
* @exception  IOException  if an I/O error occurs.
* @see        java.io.FilterOutputStream#out
*/
public final void writeLong(long v) throws IOException {
    writeBuffer[0] = (byte)(v >>> 56);
    writeBuffer[1] = (byte)(v >>> 48);
    writeBuffer[2] = (byte)(v >>> 40);
    writeBuffer[3] = (byte)(v >>> 32);
    writeBuffer[4] = (byte)(v >>> 24);
    writeBuffer[5] = (byte)(v >>> 16);
    writeBuffer[6] = (byte)(v >>>  8);
    writeBuffer[7] = (byte)(v >>>  0);
    out.write(writeBuffer, 0, 8);
    incCount(8);
}

readFrom

readFrom是与writeTo相对应的方法,从输入流中利用选定的回收站模式读取numLongs的定长数据。

protected void readFrom(HollowBlobInput in, ArraySegmentRecycler memoryRecycler, long numLongs) throws
        IOException {
    int segmentSize = 1 << memoryRecycler.getLog2OfLongSegmentSize();
    int segment = 0;

    if(numLongs == 0)
        return;

    // 空歌白石:输入流的最大长度
    long fencepostLong = in.readLong();

    // 空歌白石:待读取的长度。
    while(numLongs > 0) {
        long longsToCopy = Math.min(segmentSize, numLongs);

        unsafe.putLong(segments[segment], (long) Unsafe.ARRAY_LONG_BASE_OFFSET, fencepostLong);

        // 空歌白石:已经写入的长度
        int longsCopied = 1;

        while(longsCopied < longsToCopy) {
            long l = in.readLong();
            unsafe.putLong(segments[segment], (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (8 * longsCopied++), l);
        }

        if(numLongs > longsCopied) {
            unsafe.putLong(segments[segment], (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (8 * longsCopied), in.readLong());
            fencepostLong = segments[segment][longsCopied];
        }

        segment++;
        
        // 空歌白石:计算剩余需要读取的长度,通过需要读取的长度减去已经读取的长度计算得来。
        numLongs -= longsCopied;
    }
}

FixedLengthElementArray

FixedLengthElementArray在继承SegmentedLongArray基础上实现FixedLengthData接口的行为。

构造方法与属性

可以看出,FixedLengthElementArray的构造方法中依赖SegmentedLongArray的构造方法,除此之外,也包含了需要存储的segment长度大小,以及byte为的掩码值。

public class FixedLengthElementArray extends SegmentedLongArray implements FixedLengthData {

    private static final Unsafe unsafe = HollowUnsafeHandle.getUnsafe();

    private final int log2OfSegmentSizeInBytes;
    private final int byteBitmask;

    public FixedLengthElementArray(ArraySegmentRecycler memoryRecycler, long numBits) {
        super(memoryRecycler, ((numBits - 1) >>> 6) + 1);

        this.log2OfSegmentSizeInBytes = log2OfSegmentSize + 3;
        this.byteBitmask = (1 << log2OfSegmentSizeInBytes) - 1;
    }

    // 空歌白石:此处省略其他方法
}

newFrom

FixedLengthElementArraynewFrom方法实现了将HollowBlobInput写入到SegmentedLongArraylong[][]的功能,逻辑并不是太复杂。

public static FixedLengthElementArray newFrom(HollowBlobInput in, ArraySegmentRecycler memoryRecycler)
        throws IOException {

    long numLongs = VarInt.readVLong(in);

    return newFrom(in, memoryRecycler, numLongs);
}

public static FixedLengthElementArray newFrom(HollowBlobInput in, ArraySegmentRecycler memoryRecycler, long numLongs)
        throws IOException {

    FixedLengthElementArray arr = new FixedLengthElementArray(memoryRecycler, numLongs * 64);

    arr.readFrom(in, memoryRecycler, numLongs);

    return arr;
}

set

set方法实现了按照给定index将数据value写入到数组中的功能。

@Override
public void setElementValue(long index, int bitsPerElement, long value) {
    long whichLong = index >>> 6;
    int whichBit = (int) (index & 0x3F);

    set(whichLong, get(whichLong) | (value << whichBit));

    int bitsRemaining = 64 - whichBit;

    if (bitsRemaining < bitsPerElement)
        set(whichLong + 1, get(whichLong + 1) | (value >>> bitsRemaining));
}

index & 0x3F

除以上计算逻辑外,着重讲解下为何要将index0x3F进行操作(index & 0x3F)?

首先看下0x3F是什么,0x3F对应的二进制数是0011 1111,对应的十进制是:1*2^5+1*2^4+1*2^3+1*2^2+1*2^1+1*2^0 = 630x3F与常量做与运算实质是保留常量(转换为二进制形式)的后6位数,既取值区间为[0,63]。

扩展下,比较典型的另外一个应用是0x7F0x7F对应的二进制数:0111 1111,对应的十进制是:1*2^6+1*2^5+1*2^4+1*2^3+1*2^2+1*2^1+1*2^0 = 1270x7F与常量做与运算实质是保留常量(转换为二进制形式)的后7位数,既取值区间为[0,127]。

0x7f & 256
   0111 1111 -------- 127
&  1111 1111 -------- 256
-------------------------
   0111 1111 -------- 127

0x7f & 10
   0111 1111 -------- 127
&  0000 1010 -------- 10
-----------------------
   0000 1010 -------- 10

下文中会用到的0x07也是类似功能,0x07与常量做与运算实质是保留常量(转换为二进制形式)的后4位数,既取值区间为[0,16]。

get

FixedLengthElementArrayget方法分为两种:getElementValuegetLargeElementValue,以64位长度为区分,小于64使用getElementValue,否则使用getLargeElementValue

@Override
public long getElementValue(long index, int bitsPerElement) {
    return getElementValue(index, bitsPerElement, ((1L << bitsPerElement) - 1));
}

@Override
public long getElementValue(long index, int bitsPerElement, long mask) {
    long whichByte = index >>> 3;
    int whichBit = (int) (index & 0x07);

    int whichSegment = (int) (whichByte >>> log2OfSegmentSizeInBytes);

    long[] segment = segments[whichSegment];
    long elementByteOffset = (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (whichByte & byteBitmask);
    long longVal = unsafe.getLong(segment, elementByteOffset);
    long l = longVal >>> whichBit;

    return l & mask;
}

@Override
public long getLargeElementValue(long index, int bitsPerElement) {
    long mask = bitsPerElement == 64 ? -1 : ((1L << bitsPerElement) - 1);
    return getLargeElementValue(index, bitsPerElement, mask);
}

@Override
public long getLargeElementValue(long index, int bitsPerElement, long mask) {
    long whichLong = index >>> 6;
    int whichBit = (int) (index & 0x3F);

    long l = get(whichLong) >>> whichBit;

    int bitsRemaining = 64 - whichBit;

    if (bitsRemaining < bitsPerElement) {
        whichLong++;
        l |= get(whichLong) << bitsRemaining;
    }

    return l & mask;
}

copyBits

实现从一个FixedLengthData将byte数组复制到目标集合中的目的,按照每64位一段进行复制。

@Override
public void copyBits(FixedLengthData copyFrom, long sourceStartBit, long destStartBit, long numBits) {
    if(numBits == 0)
        return;
    
    if ((destStartBit & 63) != 0) {
        int fillBits = (int) Math.min(64 - (destStartBit & 63), numBits);
        long fillValue = copyFrom.getLargeElementValue(sourceStartBit, fillBits);
        setElementValue(destStartBit, fillBits, fillValue);

        destStartBit += fillBits;
        sourceStartBit += fillBits;
        numBits -= fillBits;
    }

    long currentWriteLong = destStartBit >>> 6;

    while (numBits >= 64) {
        long l = copyFrom.getLargeElementValue(sourceStartBit, 64, -1);
        set(currentWriteLong, l);
        numBits -= 64;
        sourceStartBit += 64;
        currentWriteLong++;
    }

    if (numBits != 0) {
        destStartBit = currentWriteLong << 6;

        long fillValue = copyFrom.getLargeElementValue(sourceStartBit, (int) numBits);
        setElementValue(destStartBit, (int) numBits, fillValue);
    }
}

increment

increment实现了将新增的数据写入到已存在内存的功能。

@Override
public void incrementMany(long startBit, long increment, long bitsBetweenIncrements, int numIncrements) {
    long endBit = startBit + (bitsBetweenIncrements * numIncrements);
    for(; startBit < endBit; startBit += bitsBetweenIncrements) {
        increment(startBit, increment);
    }
}

public void increment(long index, long increment) {
    long whichByte = index >>> 3;
    int whichBit = (int) (index & 0x07);

    int whichSegment = (int) (whichByte >>> log2OfSegmentSizeInBytes);

    long[] segment = segments[whichSegment];
    long elementByteOffset = (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (whichByte & byteBitmask);
    long l = unsafe.getLong(segment, elementByteOffset);

    unsafe.putLong(segment, elementByteOffset, l + (increment << whichBit));

    /// update the fencepost longs
    if((whichByte & byteBitmask) > bitmask * 8 && (whichSegment + 1) < segments.length) {
        unsafe.putLong(segments[whichSegment + 1], (long) Unsafe.ARRAY_LONG_BASE_OFFSET, segments[whichSegment][bitmask + 1]);
    }
    if((whichByte & byteBitmask) < 8 && whichSegment > 0) {
        unsafe.putLong(segments[whichSegment - 1], (long) Unsafe.ARRAY_LONG_BASE_OFFSET + (8 * (bitmask + 1)), segments[whichSegment][0]);
    }
}

clear

clearElementValue负责清空指定index位的数据,这里并不能回收内存,而只是将位置标记为不可用。如果需要完整回收,需要使用destroy方法。

@Override
public void clearElementValue(long index, int bitsPerElement) {
    long whichLong = index >>> 6;
    int whichBit = (int) (index & 0x3F);

    long mask = ((1L << bitsPerElement) - 1);

    set(whichLong, get(whichLong) & ~(mask << whichBit));

    int bitsRemaining = 64 - whichBit;

    if (bitsRemaining < bitsPerElement)
        set(whichLong + 1, get(whichLong + 1) & ~(mask >>> bitsRemaining));
}

FixedLengthMultipleOccurrenceElementArray

FixedLengthMultipleOccurrenceElementArray基于FixedLengthElementArray实现固定长度的内存池,是对FixedLengthElementArray的一种优化,此类实现了重复使用FixedLengthElementArray的功能。

FixedLengthMultipleOccurrenceElementArray将固定位宽元素的多个实例存储在节点列表中,更多的情况下会被用于Hollow的索引功能实现。

例如,FixedLengthMultipleOccurrenceElementArray可以在节点列表的不同索引处存储多个6位元素。在底层,它使用 FixedLengthElementArray 来实现紧凑存储,允许插入固定长度元素的多个实例。 FixedLengthMultipleOccurrenceElementArray至少保持足够的空间来容纳每个元素的最大出现次,这意味着如果一个节点索引有 8 个元素,它将包含足够的空间来存储每个索引处至少 8 个元素。

任何索引的空间不足都会触发一个相对昂贵的调整大小操作,在该操作中,我们创建先前存储的倍数(当前为 1.5 倍)的存储并复制项目,但这可以通过传递对每个最大元素的更好猜测来缓解节点。

请注意,此类当前设计用于相对较小的 bitsPerElement - 它不适用于大于 60 的 bitsPerElement。

构造方法和属性

// 空歌白石:扩容倍数,默认为1.5倍
private static final double RESIZE_MULTIPLE = 1.5;
private static final long NO_ELEMENT = 0L;

private final ArraySegmentRecycler memoryRecycler;
// 空歌白石:空节点属性。
private final FixedLengthElementArray nodesWithOrdinalZero;
private final int bitsPerElement;
private final long elementMask;
private final long numNodes;

// 空歌白石:真实存储数据的属性。
private volatile FixedLengthElementArray storage;
private volatile int maxElementsPerNode;

public FixedLengthMultipleOccurrenceElementArray(ArraySegmentRecycler memoryRecycler,
        long numNodes, int bitsPerElement, int maxElementsPerNodeEstimate) {

    nodesWithOrdinalZero = new FixedLengthElementArray(memoryRecycler, numNodes);

    storage = new FixedLengthElementArray(memoryRecycler, numNodes * bitsPerElement * maxElementsPerNodeEstimate);

    this.memoryRecycler = memoryRecycler;
    this.bitsPerElement = bitsPerElement;
    this.elementMask = (1L << bitsPerElement) - 1;
    this.numNodes = numNodes;
    this.maxElementsPerNode = maxElementsPerNodeEstimate;
}

add

public void addElement(long nodeIndex, long element) {
    if (element > elementMask) {
        throw new IllegalArgumentException("Element " + element + " does not fit in "
                + bitsPerElement + " bits");
    }
    if (nodeIndex >= numNodes) {
        throw new IllegalArgumentException("Provided nodeIndex  " + nodeIndex
                + " greater then numNodes " + numNodes);
    }
    if (element == NO_ELEMENT) {
        // we use 0 to indicate an "empty" element, so we have to store ordinal zero here
        nodesWithOrdinalZero.setElementValue(nodeIndex, 1, 1);
        return;
    }
    long bucketStart = nodeIndex * maxElementsPerNode * bitsPerElement;
    long currentIndex;
    int offset = 0;
    do {
        currentIndex = bucketStart + offset * bitsPerElement;
        offset++;
    } while (storage.getElementValue(currentIndex, bitsPerElement, elementMask) != NO_ELEMENT
            && offset < maxElementsPerNode);

    if (storage.getElementValue(currentIndex, bitsPerElement, elementMask) != NO_ELEMENT) {
        // we're full at this index - resize, then figure out the new current index
        resizeStorage();
        currentIndex = nodeIndex * maxElementsPerNode * bitsPerElement + offset * bitsPerElement;
    }

    /* we're adding to the first empty spot from the beginning of the bucket - this is
    * preferable to adding at the end because we want our getElements method to be fast, and
    * it's okay for addElement to be comparatively slow */
    storage.setElementValue(currentIndex, bitsPerElement, element);
}

get

public List<Long> getElements(long nodeIndex) {
    long bucketStart = nodeIndex * maxElementsPerNode * bitsPerElement;

    List<Long> ret = new ArrayList<>();

    if (nodesWithOrdinalZero.getElementValue(nodeIndex, 1, 1) != NO_ELEMENT) {
        // 0 indicates an "empty" element, so we fetch ordinal zeros from nodesWithOrdinalZero
        ret.add(NO_ELEMENT);
    }

    for (int offset = 0; offset < maxElementsPerNode; offset++) {
        long element = storage.getElementValue(bucketStart + offset * bitsPerElement, bitsPerElement, elementMask);

        if (element == NO_ELEMENT) {
            break; // we have exhausted the elements at this index
        }

        ret.add(element);
    }

    return ret;
}

resize

private void resizeStorage() {
    int currentElementsPerNode = maxElementsPerNode;
    int newElementsPerNode = (int) (currentElementsPerNode * RESIZE_MULTIPLE);

    if (newElementsPerNode <= currentElementsPerNode) {
        throw new IllegalStateException("cannot resize fixed length array from "
                + currentElementsPerNode + " to " + newElementsPerNode);
    }

    FixedLengthElementArray newStorage = new FixedLengthElementArray(memoryRecycler, numNodes * bitsPerElement * newElementsPerNode);

    LongStream.range(0, numNodes).forEach(nodeIndex -> {
        long currentBucketStart = nodeIndex * currentElementsPerNode * bitsPerElement;
        long newBucketStart = nodeIndex * newElementsPerNode * bitsPerElement;

        for (int offset = 0; offset < currentElementsPerNode; offset++) {
            long element = storage.getElementValue(currentBucketStart + offset * bitsPerElement,
                    bitsPerElement, elementMask);

            if (element == NO_ELEMENT) {
                break; // we have exhausted the elements at this index
            }

            newStorage.setElementValue(newBucketStart + offset * bitsPerElement, bitsPerElement, element);
        }
    });

    // 空歌白石:先销毁现有存储
    storage.destroy(memoryRecycler);

    // 空歌白石:再将storage的指针指向新建的storage
    storage = newStorage;

    // 空歌白石:移动最大元素的指针。
    maxElementsPerNode = newElementsPerNode;
}

destory

销毁使用的FixedLengthElementArray的回收能力。

public void destroy() {
    storage.destroy(memoryRecycler);
}

FixedLengthDataFactory

FixedLengthData包含EncodedLongBufferFixedLengthElementArray两种实现方式,各有各的特点。

  1. EncodedLongBuffer使用BlobByteBuff作为数据存储,即一个列表指针数组,对应的MomoryMode为SHARED_MEMORY_LAZY
  2. FixedLengthElementArray自己维护了一个二维数组,对应的MomoryMode为ON_HEAP

基于此,在创建FixedLengthData时,也就有不同的工厂方法,如下FixedLengthDataFactory的实现。

public class FixedLengthDataFactory {

    private static final Logger LOG = Logger.getLogger(FixedLengthDataFactory.class.getName());

    public static FixedLengthData get(HollowBlobInput in, MemoryMode memoryMode, ArraySegmentRecycler memoryRecycler) throws IOException {

        if (memoryMode.equals(MemoryMode.ON_HEAP)) {
            return FixedLengthElementArray.newFrom(in, memoryRecycler);
        } else if (memoryMode.equals(MemoryMode.SHARED_MEMORY_LAZY)) {
            return EncodedLongBuffer.newFrom(in);
        } else {
            throw new UnsupportedOperationException("Memory mode " + memoryMode.name() + " not supported");
        }
    }

    public static void destroy(FixedLengthData fld, ArraySegmentRecycler memoryRecycler) {
        if (fld instanceof FixedLengthElementArray) {
            ((FixedLengthElementArray) fld).destroy(memoryRecycler);
        } else if (fld instanceof EncodedLongBuffer) {
            LOG.warning("Destroy operation is a no-op in shared memory mode");
        } else {
            throw new UnsupportedOperationException("Unknown type");
        }
    }
}

总结

本文全面分析和讲解了Hollow中关于内存模型的实现,从文中可以看出,Hollow对于内存的优化核心点是基于ByteBuffer来完成的。

这个思想并不是Hollow独创,在很多的开源项目中都有使用,比较典型的例子就是nettynettybuffer包就是netty针对内存的优化实现。有兴趣的朋友也可以看下netty的源码:netty-buffer

结束语

Hollow的内存实现与编码逻辑密不可分,本计划在一篇文章中全部梳理完成,但是奈何文章长度限制,只能分为两篇文章。有兴趣的朋友可以继续阅读【Netflix Hollow系列】深入Hollow编码的实现逻辑

参考文献