HotSpot 虛擬機器物件探祕

語言: CN / TW / HK

theme: geek-black

這是我參與11月更文挑戰的第13天,活動詳情檢視:2021最後一次更文挑戰

物件的記憶體佈局

在 HotSpot 虛擬機器中,物件的記憶體佈局分為以下 3 塊區域:

  • 物件頭(Header)
  • 例項資料(Instance Data)
  • 對齊填充(Padding)

hot01.png

物件頭

物件頭記錄了物件在執行過程中所需要使用的一些資料:

  • 雜湊碼
  • GC 分代年齡
  • 鎖狀態標誌
  • 執行緒持有的鎖
  • 偏向執行緒 ID
  • 偏向時間戳

物件頭可能包含型別指標,通過該指標能確定物件屬於哪個類。如果物件是一個數組,那麼物件頭還會包括陣列長度。

例項資料

例項資料部分就是成員變數的值,其中包括父類成員變數和本類成員變數。

對齊填充

用於確保物件的總長度為 8 位元組的整數倍。

HotSpot VM 的自動記憶體管理系統要求物件的大小必須是 8 位元組的整數倍。而物件頭部分正好是 8 位元組的倍數(1 倍或 2 倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

對齊填充並不是必然存在,也沒有特別的含義,它僅僅起著佔位符的作用。

物件的建立過程

類載入檢查

虛擬機器在解析.class檔案時,若遇到一條 new 指令,首先它會去檢查常量池中是否有這個類的符號引用,並且檢查這個符號引用所代表的類是否已被載入、解析和初始化過。如果沒有,那麼必須先執行相應的類載入過程。

為新生物件分配記憶體

物件所需記憶體的大小在類載入完成後便可完全確定,接下來從堆中劃分一塊對應大小的記憶體空間給新的物件。分配堆中記憶體有兩種方式:

  • 指標碰撞 如果 Java 堆中記憶體絕對規整(說明採用的是複製演算法標記整理法),空閒記憶體和已使用記憶體中間放著一個指標作為分界點指示器,那麼分配記憶體時只需要把指標向空閒記憶體挪動一段與物件大小一樣的距離,這種分配方式稱為指標碰撞
  • 空閒列表 如果 Java 堆中記憶體並不規整,已使用的記憶體和空閒記憶體交錯(說明採用的是標記-清除法,有碎片),此時沒法簡單進行指標碰撞, VM 必須維護一個列表,記錄其中哪些記憶體塊空閒可用。分配之時從空閒列表中找到一塊足夠大的記憶體空間劃分給物件例項。這種方式稱為空閒列表

初始化

分配完記憶體後,為物件中的成員變數賦上初始值,設定物件頭資訊,呼叫物件的建構函式方法進行初始化。

至此,整個物件的建立過程就完成了。

物件的訪問方式

所有物件的儲存空間都是在堆中分配的,但是這個物件的引用卻是在堆疊中分配的。也就是說在建立一個物件時兩個地方都分配記憶體,在堆中分配的記憶體實際建立這個物件,而在堆疊中分配的記憶體只是一個指向這個堆物件的指標(引用)而已。 那麼根據引用存放的地址型別的不同,物件有不同的訪問方式。

控制代碼訪問方式

堆中需要有一塊叫做“控制代碼池”的記憶體空間,控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。

引用型別的變數存放的是該物件的控制代碼地址(reference)。訪問物件時,首先需要通過引用型別的變數找到該物件的控制代碼,然後根據控制代碼中物件的地址找到物件。

hot02.png

直接指標訪問方式

引用型別的變數直接存放物件的地址,從而不需要控制代碼池,通過引用能夠直接訪問物件。但物件所在的記憶體空間需要額外的策略儲存物件所屬的類資訊的地址。

hot03.png

需要說明的是,HotSpot 採用第二種方式,即直接指標方式來訪問物件,只需要一次定址操作,所以在效能上比控制代碼訪問方式快一倍。但像上面所說,它需要額外的策略來儲存物件在方法區中類資訊的地址。