JVM是如何建立一個物件的?

程序员世杰發表於2024-07-09

哈嘍,大家好🎉,我是世傑

本文我為大家介紹面試官經常考察的「Java物件建立流程」

照例在開頭留一些面試考察內容~~

面試連環call

  1. Java物件建立的流程是什麼樣?
  2. JVM執行new關鍵字時都有哪些操作?
  3. JVM在頻繁建立物件時,如何保證執行緒安全?
  4. Java物件的記憶體佈局是什麼樣的?
  5. 物件頭都儲存哪些資料?

帶著這些問題,讓我們開始吧!🎉🎉🎉


1. 物件建立流程

當虛擬機器遇到一個位元組碼 new 指令的時候,首先去檢查這個指令的引數是否能夠在常量池中定位到一個類的符號引用。並且檢查這個符號引用代表的類是否被虛擬機器類載入器載入。如果沒有,必須先執行類載入的流程。(PS:類載入的過程可以看我之前的文章)

new指令對應到語言層面上講是,new 關鍵詞物件克隆物件序列化等。

類的生命週期

在類的檢查透過過後

1、虛擬機器就會為新生成物件分配記憶體。物件所需要的記憶體大小在類載入的時候決定。

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

3、之後要進行物件進行初始化設定,比如後設資料、物件的雜湊編碼、物件的 GC 分代年齡偏向鎖狀態等資訊這些資訊都用於存放到物件頭(Object Header)中。

4、執行 new 指令之後會接著執行構造器方法,把物件按照程式設計師的意願進行初始化(構造方法)

這樣一個真正可用的物件才算完全產生出來。

image

『總結物件建立的過程』

  • 類載入檢查
  • 分配記憶體
  • 初始化零值
  • 設定物件頭
  • 執行init方法,進行初始化

2. 物件記憶體佈局

在詳細聊載入流程之前,先說說物件在JVM堆中的記憶體佈局(這裡講的HotSpot虛擬機器物件結構)

被JVM載入物件內部結構分為:物件頭例項資料對齊填充

img

2.1 物件頭

  • 物件標記(Mark Word),如雜湊碼(HashCode)、GC分代年齡鎖狀態標誌執行緒持有的鎖偏向執行緒ID偏向時間戳等。這部分我們稱之為"Mard Word"。
  • 類元資訊(Class Pointer),即物件指向它的類後設資料的指標,虛擬機器透過這個指標來確定這個物件是哪個類的例項。
  • 陣列長度(Length),如果物件是一個Java陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料,因為虛擬機器可以透過普通Java物件的後設資料資訊確定Java物件的大小,但是從陣列的後設資料中無法確定陣列的大小。

2.2 例項資料

  • 例項資料部分是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。無論是從父類中繼承下來的,還是在子類中定義的,都需要記錄下來。HotSpot虛擬機器預設的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oop,從分配策略中可以看出,相同寬度的欄位總是分配到一起

  • 存放類的屬性(Field)資料資訊,包括父類的屬性資訊,如果是陣列的例項部分還包括陣列的長度

  • 這部分記憶體按4位元組對齊

2.3 記憶體填充/對齊填充

  • 虛擬機器要求物件起始地址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊。

3. 建立流程詳解

建立物件在判斷類載入之後,還會判斷記憶體是否規整,根據判斷結構選擇使用空閒列表還是指標碰撞的記憶體分配方式,在分配記憶體時還會考慮執行緒併發處理,使用CAS或者是TLAB來處理,然後在執行初始化零值、設定物件頭、執行<init>方法

img

3.1 分配記憶體

類載入檢查透過後,那就要為例項化的物件分配記憶體。物件所需記憶體的大小在類載入完成後便完全確定(物件記憶體佈局),為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。

『分配方式』

根據Java堆中是否規整有兩種記憶體的分配方式:(Java堆是否規整由所採用的垃圾收集器是否帶有壓縮整理功能決定)

  • 指標碰撞(Bump the pointer)
    Java堆中的記憶體是規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,分配記憶體也就是把指標向空閒空間那邊移動一段與記憶體大小相等的距離。
  • 空閒列表(Free List)
    Java堆中的記憶體不是規整的,已使用的記憶體和空閒的記憶體相互交錯,虛擬機器維護一張列表,記錄哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。

b5654999074be8496a4208e703c259efc42ae8

『併發處理』

物件頻繁分配的過程中,即使只修改一個指標所指向的位置,但是在併發的情況下也不是執行緒安全的,可能出現正在給 A 物件分配記憶體,指標還沒有來得及修改,物件 B 又同時使用原來的指標進行內分配的情況。需要藉助以下方式實現執行緒安全

  • CAS(compare and swap)對分配記憶體空間的動作進行同步處理,虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。
  • 本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB),把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體。

3.2 初始化零值

記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這裡的零值是指JAVA中欄位預設的值

  • 如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。
  • 這一步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
  • 注意這一步賦值是物件例項欄位零值,跟類載入過程中連結中的準備階段做區分,準備是為類static變數賦零值

3.3 設定物件頭

初始化零值之後,虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的後設資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭Object Header之中

物件內部結構

3.4 執行init方法

完成上述流程,其實已經完成了虛擬機器中記憶體的建立,但是我們在 Java 執行 new 建立物件的角度才剛剛開始,我們還需要呼叫構造方法初始化物件(可能還需要在此前後呼叫父類的構造方法、初始化塊等)。進行 Java 物件的初始化。


參考文章

十分鐘搞懂Java引用、物件和記憶體

Java物件建立流程

JVM 從入門到放棄之 Java 物件建立過程

JAVA物件的建立及記憶體分配詳解

相關文章