1. 程式人生 > >HotSpot的物件模型(5)

HotSpot的物件模型(5)

Java物件通過Oop來表示。Oop指的是 Ordinary Object Pointer(普通物件指標)。在 Java 建立物件例項的時候建立,用於表示物件的例項資訊。也就是說,在 Java 應用程式執行中每建立一個 Java 物件,在 JVM 內部都會建立一個 Oop 物件來表示 Java 物件。
Oop涉及到的相關類的繼承關係如下圖所示。

 

1、oopDesc類

oopDesc的一個別名為oop,所以HotSpot中一般會使用oop來表示oopDesc型別。

oopDesc 是 所 有 的 類 名 為 xxxOopDesc 格 式 的 類 的 基 類 , 這 些 類 的 實 例 表 示 Java 對 象,所以xxxOopDesc 格式的類中會宣告一些儲存 Java 物件的欄位,並且也可以直接被 C++獲取。類及重要屬性的定義如下:

位置:/openjdk/hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
...
private:
 volatile markOop _mark; 
 union _metadata {
    Klass*   _klass;
    narrowKlass _compressed_klass;
 } _metadata;
...
}

Java物件記憶體佈局主要分為header(頭部)和fields(例項欄位)。header由_mark和_metadata組成。_mark欄位儲存了Java物件的一些資訊,如GC年齡,鎖狀態等;_metadata使用聯合體(union)來宣告 ,這樣是為了在 64 位機器上能對指標進行壓縮。因為從32位平臺到64位時,主要就是指標由4位元組變為了8位元組,所以通常64位HotSpot消耗的記憶體會比32位的大,造成堆記憶體損失,不過從JDK 1.6 update14開始,64位的JVM正式支援了-XX:+UseCompressedOops(預設開啟)。這個可以壓縮指標,起到節約記憶體佔用的作用。

在64位系統下,存放_metadata的空間大小是8位元組,_mark是8位元組,物件頭為16位元組。64位開啟指標壓縮的情況下,存放_metadata的空間大小是4位元組,_mark是8位元組,物件頭為12位元組。

啟用-XX:+UseCompressedOops命令後,主要會壓縮如下的一些物件: 

  • 每個Class的屬性指標(靜態成員變數);
  • 每個物件的屬性指標;
  • 普通物件陣列的每個元素指標。 

當然,壓縮也不是所有的指標都會壓縮,對一些特殊型別的指標,HotSpot是不會優化的,例如指向Metaspace的Class物件指標、本地變數、堆疊元素、入參、返回值和NULL指標不會被壓縮。 

64位地址分為堆的基地址+偏移量,當堆記憶體小於32GB時候,在壓縮過程中,把偏移量除以8後的結果儲存到32位地址。當解壓時再把32位地址放大8倍,所以啟用-XX:+UseCompressedOops命令的條件是堆記憶體要在4GB*8=32GB以內。具體實現方式是在機器碼中植入壓縮與解壓指令,可能會給JVM增加額外的開銷。

總結一下:

  • 如果GC堆大小在4G以下,直接砍掉高32位,避免了編碼解碼過程;
  • 如果GC堆大小在4G以上32G以下,則啟用-XX:+UseCompressedOops命令;
  • 如果GC堆大小大於32G,壓指失效,使用原來的64位。
另外Java8使用Metaspace儲存元資料,在-XX:+UseCompressedOops命令之外,額外增加了一個新選項叫做-XX:+UseCompressedClassPointer。這個選項開啟後,類元資訊中的指標也用32bit的Compressed版本。而這些指標指向的空間被稱作“Compressed Class Space”。預設大小是1G,可以通過“CompressedClassSpaceSize”調整。 

聯合體中定義的_klass或_compressed_klass指標指向的是Klass例項,這個Klass例項儲存了Java物件的實際型別,也就是Java物件所對應的Java類。 

呼叫header_size()函式獲取header佔用的記憶體空間的大小,具體實現如下: 

位置:/openjdk/hotspot/src/share/vm/oops/oop.inline.hpp
static int header_size() { 
   return sizeof(oopDesc)/HeapWordSize; 
}

計算佔用的字的大小,對於64位機器來說,一個字的大小為8位元組,所以HeapWordSize的值為8。

Java物件的header資訊可以儲存到oopDesc類中定義的_mark和_metadata屬性上,而Java物件的fields沒有在oopDesc類中定義相應的屬性來儲存,所以只能申請一定大小的空間,然後按順序進行儲存。物件欄位是存放在緊跟著oopDesc例項本身佔用的記憶體空間之後的,在獲取時只能通過偏移來取值。

opDesc 類的field_base()函式可用於獲取欄位的地址,實現如下:

位置:/openjdk/hotspot/src/share/vm/oops/oop.inline.hpp
inline void* field_base(int offset) const {
    return (void*)&(  (char*)this  )[offset];
}

offset是偏移量,計算相對於當前例項this的記憶體首地址的偏移量。

2、markOopDesc類

上面介紹oopDesc類時,可以看到定義了一個屬性_mark,而型別為markOop,其實這是markOopDesc的別名。markOopDesc類的例項可以表示Java物件頭資訊的“Mark Word",包含的資訊有雜湊碼、GC分代年齡、偏向鎖標記、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。

markOopDesc類的例項並不能表示一個具體的Java物件,而是通過一個字的各個位來表示Java物件的頭資訊。對於32位系統來說,一個字為32位(4位元組),而對於64位系統來說,一個字有64位(8位元組)。由於目前64位是主流,所以筆者不在對32位的結構進行說明。

下圖表示了在Java物件不同狀態下的Mark Word各個位區間的含義。

 

上面每一行代表物件處於某種狀態時的樣子。其中各部分的含義如下:

  • lock:2位的鎖狀態標記位,由於希望用盡可能少的二進位制位表示儘可能多的資訊,所以設定了lock標記。該標記的值不同,整個Mark Word表示的含義不同。biased_lock和lock一起表示了鎖的狀態。
  • biased_lock:物件是否啟用偏向鎖標記,只佔1個二進位制位。為1時表示物件啟用偏向鎖,為0時表示物件沒有偏向鎖。lock和biased_lock共同表示物件的鎖狀態。
  • age:佔用4個二進位制位,儲存的是Java物件的年齡。在GC中,如果物件在Survivor區複製一次,年齡增加1。當物件達到設定的閾值時,將會晉升到老年代。預設情況下,並行GC的年齡閾值為15,併發GC的年齡閾值為6。由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。
  • identity_hashcode:佔用31個二進位制位,用來儲存物件的HashCode,採用延遲載入技術。呼叫方法System.identityHashCode()計算,並會將結果寫到該物件頭中。如果當前物件的鎖狀態為偏向鎖,由於偏向鎖沒有儲存HashCode的地方,所以呼叫identityHashCode()方法會造成鎖升級,而輕量級鎖和重量級鎖所指向的lock record或monitor都有儲存HashCode的空間。hashCode 只針對 identity hash code。使用者自定義的 hashCode() 方法所返回的值不存在 Mark Word 中。Identity hash code 是未被覆寫的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。
  • thread:持有偏向鎖的執行緒ID。
  • epoch:偏向鎖的時間戳。
  • ptr_to_lock_record:輕量級鎖狀態下,指向棧中鎖記錄的指標。
  • ptr_to_heavyweight_monitor:重量級鎖狀態下,指向物件監視器Monitor的指標。

關於鎖與鎖升級相關的內容,後續文章會詳細介紹,這裡只需要大概認識一下相關的欄位即可。  

相關文章的連結如下:

1、在Ubuntu 16.04上編譯OpenJDK8的原始碼 

2、除錯HotSpot原始碼

3、HotSpot專案結構 

4、HotSpot的啟動過程 

5、HotSpot二分模型(1)

6、HotSpot的類模型(2)  

7、HotSpot的類模型(3) 

8、HotSpot的類模型(4)

關注公眾號,有HotSpot原始碼剖析系列文章!  

 

參考文章:

(1)JVM之壓縮指標(CompressedOops)

(2)JVM Anatomy Quark #23: Compressed References 

  

&n