HotSpot的對象模型(5)

Java對象經過Oop來表示。Oop指的是 Ordinary Object Pointer(普通對象指針)。在 Java 建立對象實例的時候建立,用於表示對象的實例信息。也就是說,在 Java 應用程序運行中每建立一個 Java 對象,在 JVM 內部都會建立一個 Oop 對象來表示 Java 對象。
Oop涉及到的相關類的繼承關係以下圖所示。html

 

一、oopDesc類

oopDesc的一個別名爲oop,因此HotSpot中通常會使用oop來表示oopDesc類型。java

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字節。jvm

啓用-XX:+UseCompressedOops命令後,主要會壓縮以下的一些對象: ide

  • 每一個Class的屬性指針(靜態成員變量);
  • 每一個對象的屬性指針;
  • 普通對象數組的每一個元素指針。 

固然,壓縮也不是全部的指針都會壓縮,對一些特殊類型的指針,HotSpot是不會優化的,例如指向Metaspace的Class對象指針、本地變量、堆棧元素、入參、返回值和NULL指針不會被壓縮。 函數

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

總結一下:佈局

  • 若是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類。 post

調用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的內存首地址的偏移量。

二、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的指針。

關於鎖與鎖升級相關的內容,後續文章會詳細介紹,這裏只須要大概認識一下相關的字段便可。  

相關文章的連接以下:

一、在Ubuntu 16.04上編譯OpenJDK8的源代碼 

二、調試HotSpot源代碼

三、HotSpot項目結構 

四、HotSpot的啓動過程 

五、HotSpot二分模型(1)

六、HotSpot的類模型(2)  

七、HotSpot的類模型(3) 

八、HotSpot的類模型(4)

關注公衆號,有HotSpot源碼剖析系列文章!  

 

參考文章:

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

(2)JVM Anatomy Quark #23: Compressed References 

相關文章
相關標籤/搜索