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位。
聯合體中定義的_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的內存首地址的偏移量。關於在HotSpot中計算偏移量的方法在HotSpot源碼分析之C++對象的內存布局 一文中已經介紹過,這里不再介紹。
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的指針。
關於鎖與鎖升級相關的內容,后續文章會詳細介紹,這里只需要大概認識一下相關的字段即可。
typedef class markOopDesc* markOop;
源代碼位置:runtime/basicLock.hpp
class BasicLock VALUE_OBJ_CLASS_SPEC { private: volatile markOop _displaced_header; ... }
相關文章的鏈接如下:
1、在Ubuntu 16.04上編譯OpenJDK8的源代碼
關注公眾號,有HotSpot源碼剖析系列文章!
參考文章:
(2)JVM Anatomy Quark #23: Compressed References