JVM:全面解析Java對象的創建、記憶體布局 & 訪問定位流程

  • 2019 年 10 月 25 日
  • 筆記

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/carson_ho/article/details/102364791

前言

  • 了解 Java 對象從被創建、存儲 & 怎麼被使用的整個過程十分重要
  • 對應過程則是:對象創建、對象記憶體布局、對象訪問定位的三個過程。
  • 在本文將 ,我對 Java 對象創建、對象記憶體布局、對象訪問定位的三個過程 進行了詳細介紹,希望你們會喜歡

在接下來的日子,我會推出一系列講解JVM的文章,具體如下;

1. 對象創建

  • 在開發使用時,創建 Java 對象僅僅只是是通過關鍵字new
A a = new A();
  • 可是 Java對象在虛擬機中創建則是相對複雜。今天,我將詳解Java對象在虛擬機中的創建過程

限於普通對象,不包括數組和Class對象等

1.1 創建過程

當遇到關鍵字new指令時,Java對象創建過程便開始,整個過程如下:

下面我將對每個步驟進行講解。

1.2 過程步驟

步驟1:類載入檢查

  1. 檢查 該new指令的參數 是否能在 常量池中 定位到一個類的符號引用
  2. 檢查 該類符號引用 代表的類是否已被載入、解析和初始化過

如果沒有,需要先執行相應的類載入

關於類載入請看文章:JVM)Java虛擬機:類載入的5個過程

步驟2:為對象分配記憶體

  • 虛擬機將為對象分配記憶體,即把一塊確定大小的記憶體從 Java 堆中劃分出來

對象所需記憶體的大小在類載入完成後便可完全確定

  • 關於分配記憶體,此處主要講解記憶體分配方式
  • 記憶體分配 根據 Java堆記憶體是否絕對規整 分為兩種方式:指針碰撞 & 空閑列表
  1. Java堆記憶體 規整:已使用的記憶體在一邊,未使用記憶體在另一邊
  2. Java堆記憶體 不規整:已使用的記憶體和未使用記憶體相互交錯

方式1:指針碰撞

  • 假設Java堆記憶體絕對規整,記憶體分配將採用指針碰撞
  • 分配形式:已使用記憶體在一邊,未使用記憶體在另一邊,中間放一個作為分界點的指示器
  • 那麼,分配對象記憶體 = 把指針向 未使用記憶體 移動一段 與對象大小相等的距離

方式2:空閑列表

  • 假設Java堆記憶體不規整,記憶體分配將採用 空閑列表
  • 分配形式:虛擬機維護著一個 記錄可用記憶體塊 的列表,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄

額外知識

  • 分配方式的選擇 取決於 Java堆記憶體是否規整;
  • Java堆是否規整 由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此:
    1. 使用帶 Compact 過程的垃圾收集器時,採用指針碰撞;

Serial、ParNew垃圾收集器

  1. 使用基於 Mark_sweep演算法的垃圾收集器時,採用空閑列表。

CMS垃圾收集器

特別注意

  • 對象創建在虛擬機中是非常頻繁的操作,即使僅僅修改一個指針所指向的位置,在並發情況下也會引起執行緒不安全

如,正在給對象A分配記憶體,指針還沒有來得及修改,對象B又同時使用了原來的指針來分配記憶體

所以,給對象分配記憶體會存在執行緒不安全的問題。

解決 執行緒不安全 有兩種方案:

  1. 同步處理分配記憶體空間的行為

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

  1. 把記憶體分配行為 按照執行緒 劃分在不同的記憶體空間進行
  2. 即每個執行緒在 Java堆中預先分配一小塊記憶體(本地執行緒分配緩衝(Thread Local Allocation BufferTLAB)),哪個執行緒要分配記憶體,就在哪個執行緒的TLAB上分配,只有TLAB用完並分配新的TLAB時才需要同步鎖。
  3. 虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

步驟3: 將記憶體空間初始化為零值

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

  1. 保證了對象的實例欄位在使用時可不賦初始值就直接使用(對應值 = 0)
  2. 如使用本地執行緒分配緩衝(TLAB),這一工作過程也可以提前至TLAB分配時進行。

步驟4: 對對象進行必要的設置

如,設置 這個對象是哪個類的實例、如何才能找到類的元數據資訊、對象的哈希碼、對象的GC分代年齡等資訊。

這些資訊存放在對象的對象頭中

  • 至此,從 Java 虛擬機的角度來看,一個新的 Java對象創建完畢
  • 但從 Java 程式開發來說,對象創建才剛開始,需要進行一些初始化操作。

1.3 總結

下面用一張圖總結 Java對象創建的過程


2. 對象的記憶體布局

  • 問題:在 Java 對象創建後,到底是如何被存儲在Java記憶體里的呢?
  • 答:在Java虛擬機(HotSpot)中,對象在 Java 記憶體中的 存儲布局 可分為三塊:
    1. 對象頭 存儲區域
    2. 實例數據 存儲區域
    3. 對齊填充 存儲區域

下面我會詳細說明每一塊區域。

2.1 對象頭 區域

此處存儲的資訊包括兩部分:

  • 對象自身的運行時數據(Mark Word
  1. 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等
  2. 該部分數據被設計成1個 非固定的數據結構 以便在極小的空間存儲盡量多的資訊(會根據對象狀態復用存儲空間)
  • 對象類型指針
  1. 即對象指向它的類元數據的指針
  2. 虛擬機通過這個指針來確定這個對象是哪個類的實例

特別注意

如果對象 是 數組,那麼在對象頭中還必須有一塊用於記錄數組長度的數據

因為虛擬機可以通過普通Java對象的元數據資訊確定對象的大小,但是從數組的元數據中卻無法確定數組的大小。

2.2 實例數據 區域

  • 存儲的資訊:對象真正有效的資訊

即程式碼中定義的欄位內容

  • 註:這部分數據的存儲順序會受到虛擬機分配參數(FieldAllocationStyle)和欄位在Java源碼中定義順序的影響。
// HotSpot虛擬機默認的分配策略如下:  longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers)  // 從分配策略中可以看出,相同寬度的欄位總是被分配到一起  // 在滿足這個前提的條件下,父類中定義的變數會出現在子類之前    CompactFields = true;  // 如果 CompactFields 參數值為true,那麼子類之中較窄的變數也可能會插入到父類變數的空隙之中。

2.3 對齊填充 區域

  • 存儲的資訊:佔位符

佔位作用

  • 因為對象的大小必須是8位元組的整數倍
  • 而因HotSpot VM的要求對象起始地址必須是8位元組的整數倍,且對象頭部分正好是8位元組的倍數。
  • 因此,當對象實例數據部分沒有對齊時(即對象的大小不是8位元組的整數倍),就需要通過對齊填充來補全。

2.4 總結

3. 對象的訪問定位

  • 問:建立對象後,該如何訪問對象呢?

實際上需訪問的是 對象類型數據 & 對象實例數據

  • 答:Java程式 通過 棧上的引用類型數據(reference) 來訪問Java堆上的對象

由於引用類型數據(reference)在 Java虛擬機中只規定了一個指向對象的引用,但沒定義該引用應該通過何種方式去定位、訪問堆中的對象的具體位置

所以對象訪問方式取決於虛擬機實現。目前主流的對象訪問方式有兩種:

  • 句柄 訪問
  • 直接指針 訪問

具體請看如下介紹:

4. 總結

  • 本文我對 Java 對象創建、對象記憶體布局、對象訪問定位的三個過程 進行了詳細介紹。