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:類加載檢查
- 檢查 該
new
指令的參數 是否能在 常量池中 定位到一個類的符號引用 - 檢查 該類符號引用 代表的類是否已被加載、解析和初始化過
如果沒有,需要先執行相應的類加載
關於類加載請看文章:JVM)Java虛擬機:類加載的5個過程
步驟2:為對象分配內存
- 虛擬機將為對象分配內存,即把一塊確定大小的內存從
Java
堆中劃分出來
對象所需內存的大小在類加載完成後便可完全確定
- 關於分配內存,此處主要講解內存分配方式
- 內存分配 根據 Java堆內存是否絕對規整 分為兩種方式:指針碰撞 & 空閑列表
Java
堆內存 規整:已使用的內存在一邊,未使用內存在另一邊Java
堆內存 不規整:已使用的內存和未使用內存相互交錯

方式1:指針碰撞
- 假設Java堆內存絕對規整,內存分配將採用指針碰撞
- 分配形式:已使用內存在一邊,未使用內存在另一邊,中間放一個作為分界點的指示器

- 那麼,分配對象內存 = 把指針向 未使用內存 移動一段 與對象大小相等的距離

方式2:空閑列表
- 假設Java堆內存不規整,內存分配將採用 空閑列表
- 分配形式:虛擬機維護着一個 記錄可用內存塊 的列表,在分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄
額外知識
- 分配方式的選擇 取決於
Java
堆內存是否規整; - 而
Java
堆是否規整 由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此:- 使用帶
Compact
過程的垃圾收集器時,採用指針碰撞;
- 使用帶
如
Serial、ParNew
垃圾收集器
- 使用基於
Mark_sweep
算法的垃圾收集器時,採用空閑列表。
如
CMS
垃圾收集器
特別注意
- 對象創建在虛擬機中是非常頻繁的操作,即使僅僅修改一個指針所指向的位置,在並發情況下也會引起線程不安全
如,正在給對象A分配內存,指針還沒有來得及修改,對象B又同時使用了原來的指針來分配內存
所以,給對象分配內存會存在線程不安全的問題。
解決 線程不安全 有兩種方案:
- 同步處理分配內存空間的行為
虛擬機採用
CAS
+ 失敗重試的方式 保證更新操作的原子性
- 把內存分配行為 按照線程 劃分在不同的內存空間進行
- 即每個線程在
Java
堆中預先分配一小塊內存(本地線程分配緩衝(Thread Local Allocation Buffer
,TLAB
)),哪個線程要分配內存,就在哪個線程的TLAB上
分配,只有TLAB用完並分配新的TLAB時才需要同步鎖。 - 虛擬機是否使用
TLAB
,可以通過-XX:+/-UseTLAB
參數來設定。
步驟3: 將內存空間初始化為零值
內存分配完成後,虛擬機需要將分配到的內存空間初始化為零(不包括對象頭)
- 保證了對象的實例字段在使用時可不賦初始值就直接使用(對應值 = 0)
- 如使用本地線程分配緩衝(TLAB),這一工作過程也可以提前至TLAB分配時進行。
步驟4: 對對象進行必要的設置
如,設置 這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。
這些信息存放在對象的對象頭中。
- 至此,從
Java
虛擬機的角度來看,一個新的Java
對象創建完畢 - 但從
Java
程序開發來說,對象創建才剛開始,需要進行一些初始化操作。
1.3 總結
下面用一張圖總結 Java
對象創建的過程

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

下面我會詳細說明每一塊區域。
2.1 對象頭 區域
此處存儲的信息包括兩部分:
- 對象自身的運行時數據(
Mark Word
)
- 如哈希碼(
HashCode
)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等 - 該部分數據被設計成1個 非固定的數據結構 以便在極小的空間存儲盡量多的信息(會根據對象狀態復用存儲空間)
- 對象類型指針
- 即對象指向它的類元數據的指針
- 虛擬機通過這個指針來確定這個對象是哪個類的實例
特別注意
如果對象 是 數組,那麼在對象頭中還必須有一塊用於記錄數組長度的數據
因為虛擬機可以通過普通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
對象創建、對象內存布局、對象訪問定位的三個過程 進行了詳細介紹。