深入理解Java虛擬機(二):HotSpot虛擬機對象探祕

引言

通過上篇博客,我們已經瞭解了Java虛擬機的內存區域的劃分及每個區域存儲的內容,那麼,本篇博客將以HotSpot虛擬機和內存區域Java堆爲例,繼續深入瞭解內部是如何創建、佈局和訪問的。

探祕之旅

1. 對象的創建

Java是一門面向對象的編程語言,Java程序運行過程中無時無刻都有對象被創建出來。在語言層面上,僅僅是一個new關鍵字而已,而在虛擬機中,又是怎樣一個過程呢?

1) 檢查,如果沒有被加載、解析和初始化過,執行類加載過程

當Java虛擬機遇到一條字節碼new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

2) 檢查通過,分配內存

在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務實際上便等同於把一塊確定大小的內存塊從Java堆中劃分出來。

分配內存的兩種方式
  • 指針碰撞
    假設Java堆中內存是絕對規整的,所有被使用過的內存都被放在一邊,空閒的內存被放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間方向挪動一段與對象大小相等的距離。

  • 空閒列表
    如果Java堆中的內存並不是規整的,已被使用的內存和空閒的內存相互交錯在一起,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。

線程安全問題
  • 出現場景
    在併發情況下,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。

  • 解決方案
    (1)對分配內存空間的動作進行同步處理——實際上虛擬機是採用CAS配上失敗重試的方式保證更新操作的原子性。
    (2)把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB),哪個線程要分配內存,就在哪個線程的本地緩衝區中分配,只有本地緩衝區用完了,分配新的緩存區時才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

3) 設置對象信息,完成對象構造

內存分配完成之後,虛擬機必須將分配到的內存空間(但不包括對象頭)都初始化爲零值。

接下來,Java虛擬機還要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼(實際上對象的哈希碼會延後到真正調用Object::hashCode()方法時才計算)、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。

在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了。但是從Java程序的視角看來,對象創建纔剛剛開始——構造函數,即Class文件中的()方法還沒有執行,所有的字段都爲默認的零值,對象需要的其他資源和狀態信息也還沒有按照預定的意圖構造好。

一般來說,new指令之後會接着執行init()方法,按照程序員的意願對對象進行初始化,這樣一個真正可用的對象纔算完全被構造出來。

2. 對象的內存佈局

在HotSpot虛擬機裏,對象在堆內存中的存儲佈局可以劃分爲三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

1) 對象頭(兩類信息:自身運行時數據、類型指針)

對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32個比特和64個比特,官方稱它爲“Mark Word”。

類型指針,即對象指向它的類型元數據的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不一定要經過對象本身。

2) 實例數據(對象真正存儲的有效信息)

我們在程序代碼裏面所定義的各種類型的字段內容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。

這部分的存儲順序會受到虛擬機分配策略參數(-XX:FieldsAllocationStyle參數)和字段在Java源碼中定義順序的影響。

HotSpot虛擬機默認的分配順序爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上默認的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。

3) 對齊填充(佔位符作用)

這並不是必然存在的,也沒有特別的含義。

由於HotSpot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是任何對象的大小都必須是8字節的整數倍。對象頭部分已經被精心設計成正好是8字節的倍數(1倍或者2倍),因此,如果對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。

3. 對象的訪問定位

對象訪問方式是由虛擬機實現而定的,主流的訪問方式主要有使用句柄直接指針兩種。

使用句柄

Java堆中將可能會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息。如下圖所示:

在這裏插入圖片描述
該方式優勢在於reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。

直接指針

Java堆中對象的內存佈局就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接訪問的開銷,如下圖:

在這裏插入圖片描述
該方式的最大的好處在於速度快,它節省了一次指針定位的時間開銷,由於對象訪問在Java中非常頻繁,因此這類開銷積少成多也是一項極爲可觀的執行成本。

對於虛擬機HotSpot而言,它主要使用第二種方式進行對象訪問。

總結

理論性加上專業性的知識,總結起來有些費勁了,不過一篇博客寫完之後,按照自己的理解,劃分成各個小點,標註出各個關鍵詞,就比較清楚了。下一篇,實戰OutOfMemoryError異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章