【JVM之記憶體與垃圾回收篇】對象實例化記憶體布局與訪問定位

對象實例化記憶體布局與訪問定位

從各自具體的記憶體分配上來講
new 的對象放在
對象所屬的類型資訊是放在方法區
方法當中的局部變數放在棧空間

這 new 的對象怎麼把三塊粘合到一起 就是這章的內容了

對象實例化

面試題

美團:

  • 對象在 JVM 中是怎麼存儲的?
  • 對象頭資訊裡面有哪些東西?

螞蟻金服:二面

  • Java 對象頭有什麼?

從對象創建的方式和步驟開始說

對象創建方式

  • new:最常見的方式,單例類中調用 getInstance 的靜態類方法,XXXFactory 的靜態方法
  • Class 的 newInstance 方法:反射的方式,在 JDK9 裡面被標記為過時的方法,因為只能調用空參構造器,許可權必須是 public
  • Constructor 的 newInstance(XXX):反射的方式,可以調用空參、帶參的構造器,許可權沒有要求
  • 使用 clone():不調用任何的構造器,要求當前類需要實現 Cloneable 介面中的 clone() 方法
  • 使用反序列化:反序列化一般用於 Socket 的網路傳輸,從文件中、從網路中獲取一個對象的二進位流
  • 第三方庫 Objenesis

創建對象的步驟

判斷對象對應的類是否載入、鏈接、初始化

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

為對象分配記憶體

首先計算對象佔用空間的大小,接著在堆中劃分一塊記憶體給新對象。
如果實例成員變數是引用變數,僅分配引用變數空間即可,即 4 個位元組大小

如果記憶體規整:指針碰撞

如果記憶體是規整的,那麼虛擬機將採用的是指針碰撞法(Bump The Point)來為對象分配記憶體。

意思是所有用過的記憶體在一邊,空閑的記憶體放另外一邊,中間放著一個指針作為分界點的指示器,分配記憶體就僅僅是把指針指向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是 Serial,ParNew 這種基於壓縮演算法的,虛擬機採用這種分配方式。一般使用帶有 Compact(整理)過程的收集器時,使用指針碰撞。

如果記憶體不規整
  • 虛擬表需要維護一個列表
  • 空閑列表分配

如果記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,那麼虛擬機將採用的是空閑列表來為對象分配記憶體。意思是虛擬機維護了一個列表,記錄上那些記憶體塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。這種分配方式成為了 「空閑列表(Free List)」

選擇哪種分配方式由 Java 堆是否規整所決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

處理並發問題

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

初始化分配到的記憶體空間

給對象屬性賦值的操作

  1. 屬性的默認初始化
  2. 顯示初始化/程式碼塊中的初始化(這倆並列執行,具體先後得看順序)
  3. 構造器初始化
  • 所有屬性設置默認值,保證對象實例欄位在不賦值可以直接使用

設置對象的對象頭

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

執行init方法進行初始化

在 Java 程式的視角看來,初始化才正式開始。初始化成員變數,執行實例化程式碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變數

因此一般來說(由位元組碼中跟隨 invokespecial 指令所決定),new 指令之後會接著就是執行方法,把對象按照程式設計師的意願進行初始化,這樣一個真正可用的對象才算完全創建出來。

對象實例化的過程

  • 載入類元資訊
  • 為對象分配記憶體
  • 處理並發問題
  • 屬性的默認初始化(零值初始化)
  • 設置對象頭資訊
  • 屬性的顯示初始化、程式碼塊中初始化、構造器中初始化

對象記憶體布局

對象頭

對象頭包含了兩部分,分別是

  • 運行時元數據(Mark Word)
  • 類型指針

如果是數組,還需要記錄數組的長度

運行時元數據

  • 哈希值(HashCode):對象的首地址值,換成哈希以免查找耗時
  • GC 分代年齡:年齡計數器 age,達到 16 就進入老年代了
  • 鎖狀態標誌
  • 執行緒持有的鎖
  • 偏向執行緒 ID
  • 偏向時間戳

類型指針

指向類元數據 InstanceKlass,確定該對象所屬的類型。指向的其實是方法區中存放的類元資訊

並不是所有的對象都會保留類型指針。

實例數據(Instance Data)

說明

它是對象真正存儲的有效資訊,包括程式程式碼中定義的各種類型的欄位(包括從父類繼承下來的和本身擁有的欄位)

規則

  • 父類中定義的變數會出現在子類之前
  • 相同寬度的欄位總是被分配在一起
  • 如果 CompactFields 參數為 true(默認為 true):字類的窄變數可能插入到父類變數的空隙

對齊填充(Padding)

不是必須的,也沒有特別的含義,僅僅起到佔位符的作用

小結

對象的訪問定位

圖示

JVM 是如何通過棧幀中的對象引用訪問到其內部的對象實例呢?

對象訪問的兩種方式

句柄訪問

句柄訪問就是說棧的局部變數表中,記錄的對象的引用,然後在堆空間中開闢了一塊空間,也就是句柄池

優點:

reference 中存儲穩定句柄地址,對象被移動(垃圾收集時移動對象很普遍)時只會改變句柄中實例數據指針即可,reference 本身不需要被修改。

直接指針(HotSpot 採用)

直接指針是局部變數表中的引用,直接指向堆中的實例,在對象實例中有類型指針,指向的是方法區中的對象類型數據

此時如果堆中對象的位置改變,那麼 reference 也要跟著改變。如 「垃圾回收相關演算法」中的複製演算法。

Tags: