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 buffers sub-region. Modifying the content of
* the returned buffer or this buffer affects each others 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,理論上來說在以後的版本是可以幹掉的)的介面。

參考資料

  • 題圖: sydnegeorge.com/wp-cont
  • Memory-Efficient Java Code: slideshare.net/cnbailey

推薦閱讀:

相关文章