天天看點

JVM中的對象建立、記憶體布局和通路定位

對象的建立

從語言層面來講,我們建立對象不過是使用了一個new關鍵字而已,在虛拟機中到底發生了什麼呢?

1、虛拟機遇到一條new指令的時候,首先檢查這個指令的參數能否在常量池定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已經被加載、解析、初始化過。如果沒有,執行相應的類加載過程(後面的文章會提到)。

2、類加載檢查通過後,虛拟機為新生對象配置設定記憶體,對象所需記憶體大小在類加載完成後便可完全确定。Java堆中記憶體是絕對規整的,所有用過的記憶體放一邊,空閑的記憶體放在另一半,中間放着一個指針作為分界點的訓示器。配置設定記憶體的過程就相當于把這個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種配置設定方式叫做“指針碰撞”(Bump the pointer)。如果Java堆中記憶體不是規整的,已使用的記憶體和空閑的記憶體互相交錯就沒有辦法簡單地進行指針碰撞了,而是需要維護一張表,記錄哪些記憶體塊可用,配置設定的時候從清單找出一塊足夠大的空間配置設定給對象,然後更新清單記錄,這種配置設定方式叫做“空閑清單”(Free List)。

3、記憶體配置設定完成之後将配置設定到的記憶體空間都初始化為零值。

4、JVM對對象頭(Object Header)進行必要的設定。比如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊、對象的哈希碼、對象的GC分代年齡等資訊,這些資訊都存儲在對象頭裡。

5、到目前為止,對象産生了,但是字段還全是零值,接下來會執行<init>方法,把對象按照程式員的意願進行初始化。然後才算完成了一個真正可用的對象。

對象的記憶體布局

對象在記憶體中存儲的布局可以分為3塊區域:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)

1、對象頭包含兩部分資訊,第一部分用于存儲對象自身的運作時資料(哈希碼、GC分代年齡、鎖狀态标志、線程持有鎖、偏向線程ID、偏向時間戳等);另外一部分是類型指針,指向它的類中繼資料的指針,JVM通過這個指針來确定這個對象是哪個類的執行個體。不是所有的JVM實作都必須在對象資料上保留類型指針。

2、執行個體資料部分是對象真正存儲的有效資訊,也是程式代碼種所定義的各種類型的字段内容。無論是從父類繼承下來的還是子類中定義的,都需要記錄起來,相同寬度的字段總是被配置設定到一起。

3、對齊填充部分不是必然存在的,沒有别的含義,僅僅是占位符,因為對象的大小需要是8的整數倍,此時就需要用對其填充來補全。

對象的通路定位

我們的Java程式需要棧上的reference來操作堆上的具體對象。由于reference類型在JVM中隻規定了一個指向對象的引用,沒有規定這個引用應該通過什麼樣的方式區定位通路堆中的對象,是以對象通路方式也是通過JVM決定的,目前主流的通路方式有使用句柄和直接指針兩種。

首先來看使用句柄的通路方式:

JVM中的對象建立、記憶體布局和通路定位

如果使用句柄通路方式的話,java堆會劃分出一部分記憶體用來做句柄池,這種情況下reference存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料和對象類型資料的位址。

再來看看直接指針的通路方式:

JVM中的對象建立、記憶體布局和通路定位

如果使用直接指針通路的話,那麼Java堆對象的布局中就必須加上通路類型的資料的相關資訊,而reference中存儲的直接就是對象位址。

優缺點比較:

使用句柄通路的最大好處在于reference中存儲的是穩定的句柄位址,在對象被移動的時候(這在GC的時候簡直太常見了)隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。

使用直接指針的最大好處在于速度更快,節省了以此指針定位的時間開銷,這種開銷積少成多也很可觀。Sun HotSpot使用的是直接指針通路方式。

繼續閱讀