今天來說些抽象的東西 -- 對象頭,由於我在學習的過程當中發現不少地方都關聯到了對象頭的知識點,例如JDK中的 synchronized鎖優化 和 JVM 中對象年齡升級等等。要深刻理解這些知識的原理,瞭解對象頭的概念頗有必要,並且能夠爲後面分享 synchronized 原理和 JVM 知識的時候作準備。html
Java 中經過 new 關鍵字建立一個類的實例對象,對象存於內存的堆中並給其分配一個內存地址,那麼是否想過以下這些問題:java
在 JVM 中,Java對象保存在堆中時,由如下三部分組成:api
咱們能夠在Hotspot官方文檔中找到它的描述(下圖)。從中能夠發現,它是Java對象和虛擬機內部對象都有的共同格式,由兩個字(計算機術語)組成。另外,若是對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通Java對象的元數據信息肯定Java對象的大小,可是從數組的元數據中沒法肯定數組的大小。數組
它裏面提到了對象頭由兩個字組成,這兩個字是什麼呢?咱們仍是在上面的那個Hotspot官方文檔中往上看,能夠發現還有另外兩個名詞的定義解釋,分別是 mark word 和 klass pointer。緩存
從中能夠發現對象頭中那兩個字:第一個字就是 mark word,第二個就是 klass pointer。markdown
用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等。oracle
Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。咱們打開openjdk的源碼包,對應路徑/openjdk/hotspot/src/share/vm/oops
,Mark Word對應到C++的代碼markOop.hpp
,能夠從註釋中看到它們的組成,本文全部代碼是基於Jdk1.8。ide
Mark Word在不一樣的鎖狀態下存儲的內容不一樣,在32位JVM中是這麼存的工具
在64位JVM中是這麼存的oop
雖然它們在不一樣位數的JVM中長度不同,可是基本組成內容是一致的。
即類型指針,是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
若是對象有屬性字段,則這裏會有數據信息。若是對象無屬性字段,則這裏就不會有數據。根據字段類型的不一樣佔不一樣的字節,例如boolean類型佔1個字節,int類型佔4個字節等等;
對象能夠有對齊數據也能夠沒有。默認狀況下,Java虛擬機堆中對象的起始地址須要對齊至8的倍數。若是一個對象用不到8N個字節則須要對其填充,以此來補齊對象頭和實例數據佔用內存以後剩餘的空間大小。若是對象頭和實例數據已經佔滿了JVM所分配的內存空間,那麼就不用再進行對齊填充了。
全部的對象分配的字節總SIZE須要是8的倍數,若是前面的對象頭和實例數據佔用的總SIZE不知足要求,則經過對齊數據來填滿。
爲何要對齊數據?字段內存對齊的其中一個緣由,是讓字段只出如今同一CPU的緩存行中。若是字段不是對齊的,那麼就有可能出現跨緩存行的字段。也就是說,該字段的讀取可能須要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。這兩種狀況對程序的執行效率而言都是不利的。其實對其填充的最終目的是爲了計算機高效尋址。
至此,咱們已經瞭解了對象在堆內存中的總體結構佈局,以下圖所示
概念的東西是抽象的,你說它是這樣組成的,就真的是嗎?學習是須要持懷疑的態度的,任何理論和概念只有本身證明和實踐以後才能接受它。還好 openjdk 給咱們提供了一個工具包,能夠用來獲取對象的信息和虛擬機的信息,咱們只需引入 jol-core 依賴,以下
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>
複製代碼
jol-core 經常使用的三個方法
ClassLayout.parseInstance(object).toPrintable()
:查看對象內部信息.GraphLayout.parseInstance(object).toPrintable()
:查看對象外部信息,包括引用的對象.GraphLayout.parseInstance(object).totalSize()
:查看對象總大小.爲了簡單化,咱們不用複雜的對象,本身建立一個類 D,先看無屬性字段的時候
public class D {
}
複製代碼
經過 jol-core 的 api,咱們將對象的內部信息打印出來
public static void main(String[] args) {
D d = new D();
System.out.println(ClassLayout.parseInstance(d).toPrintable());
}
複製代碼
最後的打印結果爲
能夠看到有 OFFSET、SIZE、TYPE DESCRIPTION、VALUE 這幾個名詞頭,它們的含義分別是
能夠看到,d對象實例共佔據16byte,對象頭(object header)佔據12byte(96bit),其中 mark word佔8byte(64bit),klass pointe 佔4byte,另外剩餘4byte是填充對齊的。
這裏因爲默認開啓了指針壓縮 ,因此對象頭佔了12byte,具體的指針壓縮的概念這裏就再也不闡述了,感興趣的讀者能夠本身查閱下官方文檔。jdk8版本是默認開啓指針壓縮的,能夠經過配置vm參數開啓關閉指針壓縮,-XX:-UseCompressedOops
。
若是關閉指針壓縮從新打印對象的內存佈局,能夠發現總SIZE變大了,從下圖中能夠看到,對象頭所佔用的內存大小變爲16byte(128bit),其中 mark word佔8byte,klass pointe 佔8byte,無對齊填充。
開啓指針壓縮能夠減小對象的內存使用。從兩次打印的D對象佈局信息來看,關閉指針壓縮時,對象頭的SIZE增長了4byte,這裏因爲D對象是無屬性的,讀者能夠試試增長几個屬性字段來看下,這樣會明顯的發現SIZE增加。所以開啓指針壓縮,理論上來說,大約能節省百分之五十的內存。jdk8及之後版本已經默認開啓指針壓縮,無需配置。
上面使用的是普通對象,咱們來看下數組對象的內存佈局,比較下有什麼異同
public static void main(String[] args) {
int[] a = {1};
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
複製代碼
打印的內存佈局信息,以下
能夠看到這時總SIZE爲共24byte,對象頭佔16byte,其中Mark Work佔8byte,Klass Point 佔4byte,array length 佔4byte,由於裏面只有一個int 類型的1,因此數組對象的實例數據佔據4byte,剩餘對齊填充佔據4byte。
通過以上的內容咱們瞭解了對象在內存中的佈局,瞭解對象的內存佈局和對象頭的概念,特別是對象頭的Mark Word的內容,在咱們後續分析 synchronize 鎖優化 和 JVM 垃圾回收年齡代的時候會有很大做用。
JVM中你們是否還記得對象在Suvivor中每熬過一次MinorGC,年齡就增長1,當它的年齡增長到必定程度後就會被晉升到老年代中,這個次數默認是15歲,有想過爲何是15嗎?在Mark Word中能夠發現標記對象分代年齡的分配的空間是4bit,而4bit能表示的最大數就是2^4-1 = 15。