面試題:JVM在Java堆中對對象的創建、記憶體結構、訪問方式
一、對象創建過程
1、檢查類是否已被載入
JVM遇到new指令時,首先會去檢查這個指令參數能否在常量池中定位到這個類的符號引用,檢查這個符號引用代表的類是否已被載入、解析、初始化,若沒有,則進行類載入
2、為新對象分配記憶體
類載入檢查後,JVM為新對象在堆記憶體中分配空間,記憶體大小在類載入完成後便可確定。記憶體分配方式有以下幾種:
1)指針碰撞(Bump the Pointer):若堆記憶體規整的,已用的和空閑的各佔一邊,分配記憶體就是把指針作為分界點,指針往空閑的一邊移動對象大小的空間。
2)空閑列表(Free List):若堆記憶體不規整,JVM必須維護一個記錄可用記憶體塊的列表,分配記憶體時,把列表中一塊空間分配給對象,並更新表記錄。
以上兩種在並發情況下,存在執行緒安全問題,在給對象A分配記憶體時,指針還沒來得及修改,對象B又同時使用原來的指針來分配記憶體。解決方案有兩種:
1)給分配記憶體的動作同步處理:JVM使用CAS+失敗重試,保證更新操作的原子性。
2)本地執行緒分配緩衝(TLAB Thread Local Allocation Buffer):給每個執行緒在堆記憶體中預先分配已小塊記憶體,在需要分配記憶體的執行緒的TLAB上分配,TLAB用完並分配新的TLAB時,才同步鎖定。JVM通過設置 -XX:+UseTLAB來開啟。
3、將分配到的記憶體都初始化為零值(不含對象頭)
保證了對象的實例欄位在java程式碼中不賦初始值就可以直接使用。如果使用TLAB,這一步可提前到TLAB分配時進行。
4、對對象進行其他必要的設置
如設置對象頭的內容
5、執行java程式碼中<init>方法進行初始化
以上4步完成後,對於JVM來說,新的對象已經產生了,但是對於java程式來說,對象才剛剛開始創建。
二、對象的記憶體結構
1、對象頭
1.1 標識欄位 Mark Work
用於存儲對象自身的運行時數據,如HashCode,GC分代年齡,鎖狀態標誌等
1.2 類型指針 Klass Pointer
對象指向它的類型元數據的指針,JVM通過這個指針確定該對象屬於哪個類的實例
如果對象是一個數組,對象頭中還要有一塊用於記錄數組長度的數據,因為數組長度是不確定的,無法通過元數據中的資訊推斷數組大小。
2、實例數據
對象實際存儲的有效資訊,即程式碼中定義的欄位和父類繼承下來的,存儲順序受到JVM分配策略參數(-XX:FieldAllocationStyle)和程式碼中欄位定義順序影響
3、對齊填充
不是必然存在,僅僅是起佔位符作用;由於HotSpot虛擬機的自動記憶體管理系統要求任何對象大小都必須是8位元組的整數倍,對象頭被設計成正好是8位元組的整數倍,因此實例數據部分沒有對齊8位元組的整數倍的話,就通過對齊填充來補全。
三、對象的訪問方式
java程式是通過java棧中的reference數據來操作堆中的具體對象
1、句柄訪問
java堆中劃分一塊記憶體作為句柄池,棧上的reference存的是對象的句柄地址,句柄池中包含對象實例數據和類型數據的地址資訊。
優點:垃圾收集移動對象時,只改變句柄中實例數據指針,而reference本身不需要修改。
2、直接訪問
直接指針訪問,reference存的直接是對象的地址。不需要多一次間接訪問的開銷。
優點:速度快,節省一次指針定位的時間開銷。
HotSpot虛擬機主要使用直接訪問進行對象訪問。
參考文獻:
1.《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》