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
, 那么获取的元素的实际index
是index + (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