Presto源码分析之Slice

Slice是Presto里面用来对内存高效地、自由地进行操作的接口。它在Presto里面很关键, Presto里面另外一个关键类 Block 就大量用到了它,要充分理解 Block 首先就要先搞清楚 Slice , 今天就先来分析一下 Slice

Slice的结构

我们先来看看Slice的结构。Slice里面是通过三个参数来确定一个内存地址: base , address , size

  • base 是通过JVM分配出来的内存,在JVM层面是int数组、byte数组的对象,而对Slice来说这就是我们要操作的内存块
  • address 是我们要操作的地址离 base 这个对象头的偏移量.
  • size 是我们这块内存的大小,一般来说就是 base 底层所对应的内存的大小(in bytes), 或者更小一点。

我们来看看基于 byte[] 来创建一个Slice的构造函数就比较形象了:

    /**
     * Creates a slice over the specified array.
     */
    Slice(byte[] base)
    {
        requireNonNull(base, "base is null");
        this.base = base;
        this.address = ARRAY_BYTE_BASE_OFFSET;
        this.size = base.length;
        this.retainedSize = INSTANCE_SIZE + sizeOf(base);
        this.reference = COMPACT;
    }

其中 base 就是这个 byte数组, address 是一个来自 Unsafe 类里面常量: ARRAY_BYTE_BASE_OFFSET ,这个常量表示的是: byte数组里面第一个元素的地址离整个byte数组地址头的偏移量。为什么会有这么一个偏移量?因为数组不止有裸的byte数据,还有一些元数据在这些真正的数据之前,一个数组在JVM里面的元数据的结构如下:

数组的内存结构

由上图可见JVM数组的元数据有 class pointer , flags , locks , 以及一个数组的长度 size 一共是 128 位,也就是 16 个 byte, 而 ARRAY_BYTE_BASE_OFFSET 正好是 16, 意思是说从 byte[] 对象的首地址偏移 16 个字节才是真正开始保存数据的地方。

我们来看看从Slice里面获取一个Byte数据的方法 getByte 的实现:

    public byte getByte(int index)
    {
        checkIndexLength(index, SIZE_OF_BYTE);
        return getByteUnchecked(index);
    }

    byte getByteUnchecked(int index)
    {
        return unsafe.getByte(base, address + index);
    }

现在就比较好理解了,它是在获取byte数组从 address 开始算的第 index 个元素:

  • 如果 address == ARRAY_BYTE_BASE_OFFSET ,那么获取的就是第 index 个元素。
  • 如果 address > ARRAY_BYTE_BASE_OFFSET , 那么获取的元素的实际 indexindex + (address - ARRAY_BYTE_BASE_OFFSET)

同时,从代码细节我们还可以看到另外一点: 因为 Unsafe.getXxx 是完全不做任何检查的,Slice在调用之前还是做了边界检查的(checkIndexLength)。

Slice.slice()

Slice 里面还有一个很有意思的方法: Slice.slice():

    /**
     * Returns a slice of this buffer's sub-region. Modifying the content of
     * the returned buffer or this buffer affects each other's content.
     */
    public Slice slice(int index, int length)
    {
        if ((index == 0) && (length == length())) {
            return this;
        }
        checkIndexLength(index, length);
        if (length == 0) {
            return Slices.EMPTY_SLICE;
        }

        if (reference == COMPACT) {
            return new Slice(base, address + index, length, retainedSize, NOT_COMPACT);
        }
        return new Slice(base, address + index, length, retainedSize, reference);
    }

这个方法返回的是Slice里面从 index 开始的、长度为 length 的一段,有点数据库领域的 视图 的感觉。其实它的实现也给人 视图 的感觉, 从最后一行的 return 我们可以看出, 新的Slice底层对应的还是同样的 base 对象,只是 address 变成了 address + index , 这种通过 index / offset 来实现类似 视图 的做法在Presto代码里面很多地方都能看到。

unsignedByteToInt

Slice里面还有一个从 guava 里面拷贝过来的方法 unsignedByteToInt , 蛮有意思的,值得分析一下。

这个方法是一个私有方法,Slice用它来辅助实现 compareTo 方法,compareTo 方法是对Slice底层整个byte流进行比较,而 unsignedByteToInt 是用来对每一个 byte 进行比较,比较 naive 的想法是:

直接读出每一个byte然后对获取到的 byte 进行比较不就好了么?

这样实现是不对的,原因在于,java里面的 byte 是有符号数字: 用 8 个bit位表示的数字的范围是: -128 - 127, 从纯字节的角度来理解,一个8位都是1的byte( 1111_1111 )应该是最大的byte, 但是这个字节序列对应的Java里面的byte是: -1, 用 -1 代表 1111_1111 来比较大小结果当然不对,我们可以通过下面的Java代码来验证下:

public class ByteTest {
    public static void main(String[] args) {
        byte x = -1;                  // 1111_1111
        System.out.println(x & 0xFF); // 输出 255
        x = -128;                     // 1000_0000
        System.out.println(x & 0xFF); // 输出 128
        x = -127;                     // 1000_0001
        System.out.println(x & 0xFF); // 输出 129
        x = 0;                        // 0000_0000
        System.out.println(x & 0xFF); // 输出 0
    }
}

这里也可以顺便总结一下Java里面 byte 的内存表示:

  • 第一位是符号位, 1 表示负数。
  • byte 最小值 -128 符号为 1 , 其它位是 0: 1000_0000
  • 从最小值往上数,其它位的数值开始递增,比如 -127的内存表示: 1000_0001

其它数字类型的内存表示也是类似的,比如 Long.MIN_VALUE 的内存表示是:
10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000

因此通过 byte & 0xFF 把这个有符号的byte, 变成一个int, 才能对byte的数据流进行正确的大小比较:

    private static int unsignedByteToInt(byte thisByte)
    {
        return thisByte & 0xFF;
    }

总结

Slice 在 sun.misc.Unsafe 之上封装了一个简单、好用的Java层面可以对内存进行自由操作的接口。你可以通过Slice接口来获取指定地址的Int , Short , Byte , 同样也可以对指定区域的内存的值进行设置。

为什么不直接使用 Unsafe 呢? 我理解可能有两方面的原因:一是因为Unsafe的首要目的是快,因此它的接口是不安全的,接口都不做越界检查,同时就比较难用,Slice通过包装一层,使得接口更易用;另一方面,通过对Unsafe接口进行一层代理,使得主要的核心代码不依赖 Unsafe 这个其实不是那么可靠(Unsafe本身并不是一个公开API,理论上来说在以后的版本是可以干掉的)的接口。

参考资料

  • Memory-Efficient Java Code: https://www.slideshare.net/cnbailey/memory-efficient-java

你可能感兴趣的:(Presto源码分析之Slice)