1. 程式人生 > >[Java JVM] Hotspot GC研究- 64位引用指標壓縮技術

[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位元組, 翻了一倍.