1. 程式人生 > 其它 >6.物件的例項化記憶體佈局與訪問定位

6.物件的例項化記憶體佈局與訪問定位

一、物件的例項化

大廠面試題

美團:

  1. 物件在JVM中是怎麼儲存的?
  2. 物件頭資訊裡面有哪些東西?

螞蟻金服:

二面:java物件頭裡有什麼

1.1 物件建立的方式

  1. new:最常見的方式、單例類中呼叫getInstance的靜態類方法,XXXFactory的靜態方法
  2. Class的newInstance方法:在JDK9裡面被標記為過時的方法,因為只能呼叫空參構造器,並且許可權必須為 public
  3. Constructor的newInstance(Xxxx):反射的方式,可以呼叫空參的,或者帶參的構造器
  4. 使用clone():不呼叫任何的構造器,要求當前的類需要實現Cloneable介面中的clone方法
  5. 使用序列化:從檔案中,從網路中獲取一個物件的二進位制流,序列化一般用於Socket的網路傳輸
  6. 第三方庫 Objenesis

物件建立的步驟

從位元組碼看待物件的建立過程

public class ObjectTest {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup           
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1   obj   Ljava/lang/Object;
}

1、判斷物件對應的類是否載入、連結、初始化

  1. 虛擬機器遇到一條new指令,首先去檢查這個指令的引數能否在Metaspace的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入,解析和初始化。(即判斷類元資訊是否存在)。
  2. 如果該類沒有載入,那麼在雙親委派模式下,使用當前類載入器以ClassLoader + 包名 + 類名為key進行查詢對應的.class檔案,如果沒有找到檔案,則丟擲ClassNotFoundException異常,如果找到,則進行類載入,並生成對應的Class物件。

2、為物件分配記憶體

  1. 首先計算物件佔用空間的大小,接著在堆中劃分一塊記憶體給新物件。如果例項成員變數是引用變數,僅分配引用變數空間即可,即4個位元組大小
  2. 如果記憶體規整:採用指標碰撞分配記憶體
    • 如果記憶體是規整的,那麼虛擬機器將採用的是指標碰撞法(Bump The Point)來為物件分配記憶體。
    • 意思是所有用過的記憶體在一邊,空閒的記憶體放另外一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標往空閒記憶體那邊挪動一段與物件大小相等的距離罷了。
    • 如果垃圾收集器選擇的是Serial ,ParNew這種基於壓縮演算法的,虛擬機器採用這種分配方式。一般使用帶Compact(整理)過程的收集器時,使用指標碰撞。
    • 標記壓縮(整理)演算法會整理記憶體碎片,堆記憶體一存物件,另一邊為空閒區域
  3. 如果記憶體不規整
    • 如果記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,那麼虛擬機器將採用的是空閒列表來為物件分配記憶體。
    • 意思是虛擬機器維護了一個列表,記錄上哪些記憶體塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的內容。這種分配方式成為了 “空閒列表(Free List)”
    • 選擇哪種分配方式由Java堆是否規整所決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定
    • 標記清除演算法清理過後的堆記憶體,就會存在很多記憶體碎片。

3、處理併發問題

  1. 採用CAS+失敗重試保證更新的原子性
  2. 每個執行緒預先分配TLAB - 通過設定 -XX:+UseTLAB引數來設定(區域加鎖機制)
  3. 在Eden區給每個執行緒分配一塊區域

4、初始化分配到的空間

  • 所有屬性設定預設值,保證物件例項欄位在不賦值可以直接使用

  • 給物件屬性賦值的順序:

  1. 屬性的預設值初始化
  2. 顯示初始化/程式碼塊初始化(並列關係,誰先誰後看程式碼編寫的順序)
  3. 構造器初始化

5、設定物件的物件頭

將物件的所屬類(即類的元資料資訊)、物件的HashCode和物件的GC資訊、鎖資訊等資料儲存在物件的物件頭中。這個過程的具體設定方式取決於JVM實現。

6、執行init方法進行初始化

  1. 在Java程式的視角看來,初始化才正式開始。初始化成員變數,執行例項化程式碼塊,呼叫類的構造方法,並把堆內物件的首地址賦值給引用變數
  2. 因此一般來說(由位元組碼中跟隨invokespecial指令所決定),new指令之後會接著就是執行init方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完成創建出來。

從位元組碼角度看 init 方法

/**
 * 測試物件例項化的過程
 *  ① 載入類元資訊 - ② 為物件分配記憶體 - ③ 處理併發問題  - ④ 屬性的預設初始化(零值初始化)
 *  - ⑤ 設定物件頭的資訊 - ⑥ 屬性的顯式初始化、程式碼塊中初始化、構造器中初始化
 *
 *
 *  給物件的屬性賦值的操作:
 *  ① 屬性的預設初始化 - ② 顯式初始化 / ③ 程式碼塊中初始化 - ④ 構造器中初始化
 */

public class Customer{
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客戶";
    }
    public Customer(){
        acct = new Account();
    }

}
class Account{

}

Customer類的位元組碼

 0 aload_0
 1 invokespecial #1 <java/lang/Object.<init>>
 4 aload_0
 5 sipush 1001
 8 putfield #2 <com/atguigu/java/Customer.id>
11 aload_0
12 ldc #3 <匿名客戶>
14 putfield #4 <com/atguigu/java/Customer.name>
17 aload_0
18 new #5 <com/atguigu/java/Account>
21 dup
22 invokespecial #6 <com/atguigu/java/Account.<init>>
25 putfield #7 <com/atguigu/java/Customer.acct>
28 return
  • init() 方法的位元組碼指令:
    • 屬性的預設值初始化:id = 1001;
    • 顯示初始化/程式碼塊初始化:name = "匿名客戶";
    • 構造器初始化:acct = new Account();

二、物件的記憶體佈局

記憶體佈局總結

public class Customer{
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客戶";
    }
    public Customer(){
        acct = new Account();
    }
	public static void main(String[] args) {
        Customer cust = new Customer();
    }
}
class Account{

}

圖解記憶體佈局

三、物件的訪問定位

JVM是如何通過棧幀中的物件引用訪問到其內部的物件例項呢?

定位,通過棧上reference訪問

物件的兩種訪問方式:控制代碼訪問和直接指標

1、控制代碼訪問

  1. 缺點:在堆空間中開闢了一塊空間作為控制代碼池,控制代碼池本身也會佔用空間;通過兩次指標訪問才能訪問到堆中的物件,效率低
  2. 優點:reference中儲存穩定控制代碼地址,物件被移動(垃圾收集時移動物件很普遍)時只會改變控制代碼中例項資料指標即可,reference本身不需要被修改

2、直接指標(HotSpot採用)

  1. 優點:直接指標是區域性變量表中的引用,直接指向堆中的例項,在物件例項中有型別指標,指向的是方法區中的物件型別資料
  2. 缺點:物件被移動(垃圾收集時移動物件很普遍)時需要修改 reference 的值