面試JVM(七)物件的建立
物件的建立
下圖便是 Java 物件的建立過程,我建議最好是能默寫出來,並且要掌握每一步在做什麼。
①類載入檢查: 虛擬機器遇到一條 new 指令時,首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被載入過、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
②分配記憶體: 在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體大小在類載入完成後便可確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從 Java 堆中劃分出來。分配方式有 “指標碰撞” 和 “空閒列表”
記憶體分配的兩種方式:(補充內容,需要掌握)
選擇以上兩種方式中的哪一種,取決於 Java 堆記憶體是否規整。而 Java 堆記憶體是否規整,取決於 GC 收集器的演算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製演算法記憶體也是規整的
記憶體分配併發問題(補充內容,需要掌握)
在建立物件的時候有一個很重要的問題,就是執行緒安全,因為在實際開發過程中,建立物件是很頻繁的事情,作為虛擬機器來說,必須要保證執行緒是安全的,通常來講,虛擬機器採用兩種方式來保證執行緒安全:
- CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。虛擬機器採用 CAS 配上失敗重試的方式保證更新操作的原子性。
- TLAB: 為每一個執行緒預先在Eden區分配一塊兒記憶體,JVM在給執行緒中的物件分配記憶體時,首先在TLAB分配,當物件大於TLAB中的剩餘記憶體或TLAB的記憶體已用盡時,再採用上述的CAS進行記憶體分配
③初始化零值: 記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
④設定物件頭: 初始化零值完成之後,虛擬機器要對物件進行必要的設定,例如這個物件是那個類的例項、如何才能找到類的元資料資訊、物件的雜湊嗎、物件的 GC 分代年齡等資訊。 這些資訊存放在物件頭中。 另外,根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式。
⑤執行 init 方法: 在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生了,但從 Java 程式的視角來看,物件建立才剛開始,<init>
方法還沒有執行,所有的欄位都還為零。所以一般來說,執行 new 指令之後會接著執行 <init>
方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。
3.2 物件的記憶體佈局
在 Hotspot 虛擬機器中,物件在記憶體中的佈局可以分為3塊區域:物件頭、例項資料和對齊填充。
Hotspot虛擬機器的物件頭包括兩部分資訊,第一部分用於儲存物件自身的自身執行時資料(雜湊碼、GC分代年齡、鎖狀態標誌等等),另一部分是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項。
例項資料部分是物件真正儲存的有效資訊,也是在程式中所定義的各種型別的欄位內容。
對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因為Hotspot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是物件的大小必須是8位元組的整數倍。而物件頭部分正好是8位元組的倍數(1倍或2倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。
3.3 物件的訪問定位
建立物件就是為了使用物件,我們的Java程式通過棧上的 reference 資料來操作堆上的具體物件。物件的訪問方式有虛擬機器實現而定,目前主流的訪問方式有①使用控制代碼和②直接指標兩種:
-
控制代碼: 如果使用控制代碼的話,那麼Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference 中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊;
-
直接指標: 如果使用直接指標訪問,那麼 Java 堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference 中儲存的直接就是物件的地址。
這兩種物件訪問方式各有優勢。使用控制代碼來訪問的最大好處是 reference 中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標,而 reference 本身不需要修改。使用直接指標訪問方式最大的好處就是速度快,它節省了一次指標定位的時間開銷。