[Java JVM] Hotspot GC研究- 64位引用指標壓縮技術
為什麼需要指標壓縮
在上一篇文章 [Java JVM] Hotspot GC研究- 開篇&物件記憶體佈局 中介紹物件記憶體佈局時, 曾提到過, 由於在64位CPU下, 指標的寬度是64位的, 而實際的heap區域遠遠用不到這麼大的記憶體, 使用64bit來存物件引用會造成浪費, 所以應該做點事情來節省資源.
如何做
基於以下事實:
- CPU 使用的虛擬地址是64位的, 訪問記憶體時, 必須使用64位的指標訪問記憶體物件
- java物件是分配於具體的某個記憶體位置的, 對其訪問必須使用64位地址
- 對java物件內的引用欄位進行訪問時, 必須經過虛擬機器這一層, 操作某個物件引用不管是getfield還是putfield, 都是由虛擬機器來執行. 或者簡單來說, 要改變java物件某個引用欄位, 必須經過虛擬機器的參與.
細心的你從上面一定可以看出一點線索, 由於存一個物件引用和取一個物件引用必須經過虛擬機器, 所以完全可以在虛擬機器這一層做些手腳. 對於外部來說, putfield提供的物件地址是64位的, 經過虛擬機器的轉換, 對映到32位, 然後存入物件; getfield指定目標物件的64位地址和其內部引用欄位的偏移, 取32位的資料, 然後反對映到64位記憶體地址. 對於外部來說, 只看見64位的物件放進去, 拿出來, 內部的轉換是透明的.
詳細實現
請看程式碼:
hotspot/src/share/vm/oops/oop.hpp
// In order to put or get a field out of an instance, must first check
// if the field has been compressed and uncompress it.
oop oopDesc::obj_field(int offset) const {
return UseCompressedOops ?
load_decode_heap_oop(obj_field_addr<narrowOop>(offset)) :
load_decode_heap_oop(obj_field_addr<oop>(offset));
}
void oopDesc::obj_field_put(int offset, oop value ) {
UseCompressedOops ? oop_store(obj_field_addr<narrowOop>(offset), value) :
oop_store(obj_field_addr<oop>(offset), value);
}
//補充oop和narrowOop的定義
typedef juint narrowKlass;
....
typedef class oopDesc* oop;
當存取物件引用時, 首先會檢查是否開啟了指標壓縮(UseCompressedOops), 然後呼叫不同的函式來處理. 我們來看:
//模板函式, 如果T是oop, 則訪問的是8位元組; 如果是narrowKlass, 則訪問的是4位元組
template <class T> T* oopDesc::obj_field_addr(int offset) const { return (T*) field_base(offset); }
//模板函式, 這裡有兩個分支, 核心的轉換函式是oopDesc::encode_store_heap_oop(p, v);
template <class T> void oop_store(T* p, oop v) {
if (always_do_update_barrier) {
oop_store((volatile T*)p, v);
} else {
update_barrier_set_pre(p, v);
oopDesc::encode_store_heap_oop(p, v);
// always_do_update_barrier == false =>
// Either we are at a safepoint (in GC) or CMS is not used. In both
// cases it's unnecessary to mark the card as dirty with release sematics.
update_barrier_set((void*)p, v, false /* release */); // cast away type
}
}
//壓縮指標版本, 呼叫了壓縮函式
// Encode and store a heap oop allowing for null.
void oopDesc::encode_store_heap_oop(narrowOop* p, oop v) {
*p = encode_heap_oop(v);
}
//判斷null, 否則壓縮
narrowOop oopDesc::encode_heap_oop(oop v) {
return (is_null(v)) ? (narrowOop)0 : encode_heap_oop_not_null(v);
}
//核心壓縮函式, 物件地址與base地址的差值, 再做移位
narrowOop oopDesc::encode_heap_oop_not_null(oop v) {
assert(!is_null(v), "oop value can never be zero");
assert(check_obj_alignment(v), "Address not aligned");
assert(Universe::heap()->is_in_reserved(v), "Address not in heap");
address base = Universe::narrow_oop_base();
int shift = Universe::narrow_oop_shift();
uint64_t pd = (uint64_t)(pointer_delta((void*)v, (void*)base, 1));
assert(OopEncodingHeapMax > pd, "change encoding max if new encoding");
uint64_t result = pd >> shift;
assert((result & CONST64(0xffffffff00000000)) == 0, "narrow oop overflow");
assert(decode_heap_oop(result) == v, "reversibility");
return (narrowOop)result;
}
//核心解壓縮函式, 壓縮函式反過來, base地址加上物件起始地址的偏移
oop oopDesc::decode_heap_oop_not_null(narrowOop v) {
assert(!is_null(v), "narrow oop value can never be zero");
address base = Universe::narrow_oop_base();
int shift = Universe::narrow_oop_shift();
oop result = (oop)(void*)((uintptr_t)base + ((uintptr_t)v << shift));
assert(check_obj_alignment(result), "address not aligned: " INTPTR_FORMAT, p2i((void*) result));
return result;
}
//普通指標encode版本, 直接解引用進行賦值
static inline void encode_store_heap_oop(oop* p, oop v) { *p = v; }
//普通指標decode版本, 直接返回值
static inline oop decode_heap_oop(oop v) { return v; }
從上面的程式碼我們看到了指標壓縮的程式碼, 體會下來, 做一些總結: 雖然64位的地址空間很大, 但是往往我們使用的記憶體範圍並不需要這麼多, 我們只需要能表達實際使用的記憶體範圍即可, 哪怕地址是128位的, 我們只使用了其中1G, 這種情況仍然可以使用指標壓縮; 我們需要表達的是範圍, 而不是具體值, 從上面程式碼可以看到, 實際壓縮指標儲存的是基於base地址的差值, 而這個差值的最大值, 大部分情況不會超過32bit的表示能力.
那既然壓縮後的指標是32bit, 使用指標壓縮的最大堆是4G嗎? 並非如此, 由於物件是8位元組對齊的, 因此物件起始地址最低三位總是0, 因此可以儲存時可以右移3bit, 高位空出來的3bit可以表示更高的數值, 實際上, 可以使用指標壓縮的maxHeapSize是4G * 8 = 32G.
空說乏味, 我們來實際測一下
測試java程式碼:
public class JavaTest {
/**
* @param args
*/
public static void main(String[] args) throws Exception {
// 512M個引用槽位
final int count = 512 * 1024 * 1024;
Object[] array = new Object[count];
Thread.sleep(1000000);
}
}
執行結果:
預設指標壓縮版本:
~/projects/JavaTest$ java -cp bin/ com.lqp.test.JavaTest
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10034 lqp 20 0 6835136 2.036g 15876 S 0.0 13.2 0:01.57 java可以看到, 大概使用了2.036g(約等於512M * 4)的記憶體, 其中每個引用slot佔4位元組
再看關閉指標壓縮的版本:
這裡預設heapsize已經不夠用了, 必須指定, 不然報OutOfMemoryError
~/projects/JavaTest$ java -Xms8G -XX:-UseCompressedOops -cp bin/ com.lqp.test.JavaTest
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10114 lqp 20 0 9.827g 4.055g 15624 S 0.0 26.4 0:03.52 java可以看到, 大概使用了4.055g(約等於512M * 8), 其中每個引用slot佔8位元組, 翻了一倍.