JVM:Java物件的建立、記憶體佈局 & 訪問定位 全過程解析

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

示意圖
目錄

示意圖
1. 物件建立
- 在開發使用時,建立
Java
物件僅僅只是是通過關鍵字new
:
A a = new A();
- 可是
Java
物件在虛擬機器中建立則是相對複雜。今天,我將詳解Java
物件在虛擬機器中的建立過程
限於普通物件,不包括陣列和Class物件等
1.1 建立過程
當遇到關鍵字 new
指令時,Java物件建立過程便開始,整個過程如下:

Java物件建立過程
下面我將對每個步驟進行講解。
1.2 過程步驟
步驟1:類載入檢查
new
如果沒有,需要先執行相應的類載入過程
關於類載入請看文章:
步驟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
- 物件型別指標
- 即物件指向它的類元資料的指標
- 虛擬機器通過這個指標來確定這個物件是哪個類的例項
特別注意
如果物件 是 陣列,那麼在物件頭中還必須有一塊用於記錄陣列長度的資料
因為虛擬機器可以通過普通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
物件建立、物件記憶體佈局、物件訪問定位的三個過程 進行了詳細介紹 - 在接下來的日子,我會推出一系列講解
JVM
的文章,具體如下;感興趣可持續關注 Carson_Ho的安卓開發筆記

示意圖