引入
本文主要以最常用的虛擬機器HotSpot
和最常用的記憶體區域Java
堆為例,深入探討一下HotSpot
虛擬機器在Java
堆中物件分配、佈局和訪問的全過程。
1. 物件的記憶體佈局
在HotSpot
虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分爲三個部分:物件頭(Header
)、例項資料(Instance Data
)和對齊填充(Padding
)。
2. 物件的建立
2.1 物件的建立流程
JVM
遇到一條位元組碼new
指令時:
檢查指令的引數是否能在常量池中定位到一個類的符號引用;
檢查這個符號引用代表的類是否已經被載入、解析和初始化。沒有則必須執行相應的類載入過程;
類載入檢查透過後,JVM為新生物件分配記憶體;
記憶體分配後,JVM將分配到的記憶體空間(但不包括物件頭)都初始化為零值;
設定物件頭(
Object Header
),如:這個物件是哪個類的例項、如何才能找到類的後設資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。
從虛擬機器的視角來看,完成上述流程後一個新的物件已經產生了。但是從Java
程式的視角看來,物件建立纔剛剛開始——建構函式,即Class
檔案中的<init>()
,所有的欄位都為預設的零值,物件需要的其他資源和狀態資訊也還沒有按照預定的意圖構造好。
new
指令之後會接著執行<init> ()
方法,按照程式設計師的意願對物件進行初始化,這樣一個真正可用的物件纔算完全被構造出來。
2.2 記憶體分配
物件所需記憶體的大小在類載入完成後便可完全確定,為物件分配空間的任務實際上便等同於把一塊確定大小的記憶體塊從Java
堆中劃分出來。
2.2.1 指標碰撞(Bump The Pointer
)
假設Java
堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一邊,空閒的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間方向挪動一段與物件大小相等的距離,這種分配方式稱為指標碰撞(Bump The Pointer
)。
2.2.2 空閒列表(Free List
)
假設Java
堆中的記憶體並不是規整的,已被使用的記憶體和空閒的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為空閒列表(Free List
)
選擇哪種分配方式由Java
堆是否規整決定,而Java
堆是否規整又由所採用的垃圾收集器是否帶有空間壓縮整理(Compact
)的能力決定。
2.3 堆搶佔的情況下,JVM如何保證執行緒安全
物件建立在JVM
中發生很頻繁,在併發情況下也並非執行緒安全的,可能出現正在給物件 A 分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況。
JVM
提供了兩種方案來來解決上述問題:
CAS
: 對分配記憶體空間的動作進行同步處理,實際上虛擬機器是採用CAS
配上失敗重試的方式保證更新操作的原子性;TLAB
: 是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java
堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer
,TLAB
),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩衝區中分配,只有本地緩衝區用完了,分配新的快取區時才需要同步鎖定。
虛擬機器是否使用 TLAB
,可以透過-XX:+/-UseTLAB
引數來設定。
3. 物件的訪問定位
建立好了物件自然是爲了後續使用這個物件,而物件的訪問方式實際上由JVM
實現而決定,主流的訪問方式主要有兩種:
控制代碼
直接指標
控制代碼(
Handle
):用於標識某個資源或物件的唯一識別符號。本質上是一個整數值或指標型別,它與資源實體之間的對映關係由作業系統維護。控制代碼只能透過系統函式來訪問和操作。
3.1 使用控制代碼訪問
3.1.1 訪問方式
Java
堆中劃分出一塊記憶體作為控制代碼池,reference
中儲存的是物件的控制代碼地址,控制代碼中包含物件例項資料以及型別資料各自具體的地址資訊。
3.1.2 優點
reference
中儲存的是穩定控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而reference
本身不需要被修改。
3.2 使用直接指標訪問
3.2.1 訪問方式
Java
堆中物件的記憶體佈局就必須考慮如何放置訪問型別資料的相關資訊,reference
中儲存的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷。
HotSpot採用這種方式進行物件訪問。
3.2.2 優點
速度更快,它節省了一次指標定位的時間開銷,由於物件訪問在Java
中非常頻繁,因此這類開銷積少成多也是一項極為可觀的執行成本。
參考資料
《深入理解Java虛擬機器(第3版)》