Slice是Presto裡面用來對內存高效地、自由地進行操作的介面。它在Presto裡面很關鍵, Presto裡面另外一個關鍵類 Block 就大量用到了它,要充分理解 Block 首先就要先搞清楚 Slice , 今天就先來分析一下 Slice 。
Block
Slice
我們先來看看Slice的結構。Slice裡面是通過三個參數來確定一個內存地址: base , address , size 。
base
address
size
內存塊
我們來看看基於 byte[] 來創建一個Slice的構造函數就比較形象了:
byte[]
/** * 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裡面的元數據的結構如下:
Unsafe
ARRAY_BYTE_BASE_OFFSET
元數據
由上圖可見JVM數組的元數據有 class pointer , flags , locks , 以及一個數組的長度 size 一共是 128 位,也就是 16 個 byte, 而 ARRAY_BYTE_BASE_OFFSET 正好是 16, 意思是說從 byte[] 對象的首地址偏移 16 個位元組才是真正開始保存數據的地方。
class pointer
flags
locks
16
我們來看看從Slice裡面獲取一個Byte數據的方法 getByte 的實現:
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 個元素:
index
address == ARRAY_BYTE_BASE_OFFSET
address > ARRAY_BYTE_BASE_OFFSET
index + (address - ARRAY_BYTE_BASE_OFFSET)
同時,從代碼細節我們還可以看到另外一點: 因為 Unsafe.getXxx 是完全不做任何檢查的,Slice在調用之前還是做了邊界檢查的(checkIndexLength)。
Unsafe.getXxx
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代碼裡面很多地方都能看到。
length
視圖
return
address + index
offset
Slice裡面還有一個從 guava 裡面拷貝過來的方法 unsignedByteToInt , 蠻有意思的,值得分析一下。
guava
unsignedByteToInt
這個方法是一個私有方法,Slice用它來輔助實現 compareTo 方法,compareTo 方法是對Slice底層整個byte流進行比較,而 unsignedByteToInt 是用來對每一個 byte 進行比較,比較 naive 的想法是:
compareTo
naive
直接讀出每一個byte然後對獲取到的 byte 進行比較不就好了么?
這樣實現是不對的,原因在於,java裡面的 byte 是有符號數字: 用 8 個bit位表示的數字的範圍是: -128 - 127, 從純位元組的角度來理解,一個8位都是1的byte( 1111_1111 )應該是最大的byte, 但是這個位元組序列對應的Java裡面的byte是: -1, 用 -1 代表 1111_1111 來比較大小結果當然不對,我們可以通過下面的Java代碼來驗證下:
-128
127
1111_1111
-1
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
0
1000_0000
1000_0001
其它數字類型的內存表示也是類似的,比如 Long.MIN_VALUE 的內存表示是: 10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000
Long.MIN_VALUE
因此通過 byte & 0xFF 把這個有符號的byte, 變成一個int, 才能對byte的數據流進行正確的大小比較:
byte & 0xFF
private static int unsignedByteToInt(byte thisByte) { return thisByte & 0xFF; }
Slice 在 sun.misc.Unsafe 之上封裝了一個簡單、好用的Java層面可以對內存進行自由操作的介面。你可以通過Slice介面來獲取指定地址的Int , Short , Byte , 同樣也可以對指定區域的內存的值進行設置。
sun.misc.Unsafe
Int
Short
Byte
為什麼不直接使用 Unsafe 呢? 我理解可能有兩方面的原因:一是因為Unsafe的首要目的是快,因此它的介面是不安全的,介面都不做越界檢查,同時就比較難用,Slice通過包裝一層,使得介面更易用;另一方面,通過對Unsafe介面進行一層代理,使得主要的核心代碼不依賴 Unsafe 這個其實不是那麼可靠(Unsafe本身並不是一個公開API,理論上來說在以後的版本是可以幹掉的)的介面。
推薦閱讀: