JVM虛擬機器-瞭解Java堆中物件分配、佈局和訪問的全過程

阿dun發表於2021-05-08

前言

物件是Java中最常被提到的概念,也是Java的靈魂,Java中一切皆是物件。

JVM是Java的根基,理解靈魂與根基是如何融合的,對於理解Java本身至關重要。

物件的建立

物件的建立從Java語言層面上,往往就是一個簡單的new即可搞定,而深入JVM的底層,可就複雜的多了。

JVM中物件的建立過程,分為如下5步,如圖:

image-20210508170512540

類載入檢查

虛擬機器遇到一條 new 指令後,會執行如下幾個步驟:

  1. 首先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用
  2. 檢查這個符號引用代表的類是否已被載入過解析過初始化過
  3. 如果沒有,那必須先執行相應的類載入過程。
  4. 確定物件所需的記憶體大小。
  5. 類載入檢查通過後,進入下一步驟,為新生物件分配記憶體

分配記憶體

為物件分配空間相當於分田地,就是將空餘的記憶體劃分一塊給物件。

由於不同的JVM虛擬機器,Java堆的記憶體規整情況是不同的,所以分配方式上也會有一定區別。

Java 堆是否規整,由JVM所採用的垃圾收集器是否帶有壓縮整理功能決定

記憶體空間分配方式

指標碰撞

Java堆中記憶體是規整(無記憶體碎片)時使用,即垃圾收集器有壓縮整理功能時。

原理就是將用過的記憶體放在一塊,沒用的放一塊,中間用指標做標識,分配的時候將指標向空閒記憶體區域移動

空閒列表

Java堆中記憶體不規整,即垃圾收集器無壓縮整理功能時。

使用記憶體和空閒記憶體相互交錯,JVM需維護一個列表,記錄上哪些記憶體是可用的,在分配時從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。

兩者具像化呈現如圖:

image-20210508174030000

併發時的記憶體分配

建立物件是很頻繁的事,併發建立物件的時候有一個很重要的問題,就是執行緒安全

如:程式中建立物件A和物件B,底層VM給A物件分配記憶體,指標沒來及修改,物件B同時使用原來的指標分配記憶體。

JVM一般採用一下兩種方式來保障執行緒安全。

同步處理:CAS

CAS 是樂觀鎖的一種實現方式。

所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

虛擬機器採用 CAS 配上失敗重試的方式保證更新操作的原子性。

本地執行緒分配緩衝:TLAB

  • 每一個執行緒預先在 Eden 區分配一塊兒記憶體,即為TLAB

  • JVM 在給執行緒中的物件分配記憶體時,首先在 TLAB 分配

  • 當物件大於 TLAB 中的剩餘記憶體不足或已用盡時,再採用上述的 CAS 進行記憶體分配

更多關於CAS的內容可以參考博主的另一篇文章Java併發/多執行緒-CAS原理分析

初始化零值

記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭)

這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。

設定物件頭

物件頭是虛擬機器對物件進行必要的設定

物件頭存放了關於物件的各種資訊如:

  • 物件是哪個類的例項
  • 如何才能找到類的後設資料資訊
  • 物件的雜湊碼
  • 物件的 GC 分代年齡
  • ……

根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式。

執行 init 方法

在上面工作都完成之後,從虛擬機器的視角來看,一個新的物件已經產生

從 Java 程式的視角來看,物件建立才剛開始,<init> 方法還沒有執行,所有的欄位都還為零。

所以一般來說,執行 new 指令之後會接著執行 <init> 方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。

物件的記憶體佈局

在 Hotspot 虛擬機器中,物件在記憶體中的佈局可以分為 3 塊區域。

物件頭

物件頭上面已經介紹過了,其實主要就是分為兩類:

  • 用於儲存物件自身的執行時資料
  • 型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項

例項資料

這部分是物件真正儲存的有效資訊,也是在程式中所定義的各種型別的欄位內容。

對齊填充

對齊填充不是必然存在的,沒有實際意義,就是用來補位的。

因為 Hotspot 虛擬機器的自動記憶體管理系統要求物件起始地址必須是 8 位元組的整數倍,物件的大小必須是 8 位元組的整數倍。

所以,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

物件的訪問定位

建立物件就是為了使用物件,我們的 Java 程式通過棧上的 reference 資料來操作堆上的具體物件。

物件的訪問方式由虛擬機器實現而定,目前主流的訪問方式有如下兩種方式

使用控制程式碼

Java 堆中將會劃分出一塊記憶體來作為控制程式碼池,reference 中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊,如圖:

image-20210508181954450

直接指標

如果使用直接指標訪問,那麼 Java 堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而 reference 中儲存的直接就是物件的地址。

image-20210508181849041

二者對比

使用控制程式碼來訪問的最大好處是 reference 中儲存的是穩定的控制程式碼地址,在物件被移動時只會改變控制程式碼中的例項資料指標,而 reference 本身不需要修改。

使用直接指標訪問方式最大的好處就是速度快,它節省了一次指標定位的時間開銷。

參考:《深入理解JVM虛擬機器》第三版(吹爆)

相關文章