JVM:全面解析Java對象的創建、內存佈局 & 訪問定位流程

前言

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

在接下來的日子,我會推出一系列講解JVM的文章,具體如下;感興趣可持續關注Carson_Ho的安卓開發筆記

示意圖


目錄

示意圖


1. 對象創建

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

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

1.1 創建過程

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

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. 把內存分配行爲 按照線程 劃分在不同的內存空間進行
  1. 即每個線程在 Java堆中預先分配一小塊內存(本地線程分配緩衝(Thread Local Allocation BufferTLAB)),哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時才需要同步鎖。
  2. 虛擬機是否使用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. 總結

示意圖


請點贊!因爲你的鼓勵是我寫作的最大動力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章