面試題: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版)》