深入理解 Java 虛擬機:對象的創建過程
類加載
虛擬機遇到一條 new
指令時,首先檢查,指令的參數是否能在常量池種定位到一個類的符號引用
,並且檢查這個符號引用的類是否已經被加載、解析、初始化過
,如果沒有那必須執行相應的類加載過程
。
比如:String str = null;
這就意味着類已經被加載,創建對象時這步類加載就不要執行了
分配內存
在類的加載檢查通過後,接下來虛擬機將爲新生對象分配內存
。對象所需內存大小
在類加載完成後便可以完全確定
。爲對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆種劃分出來
。
分配方式一:指針碰撞
假設 Java 堆
中的內存
是絕對規整
的,所有用過的內存放一邊,空閒的內存放另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離
,這種分配方式稱爲“指針碰撞”
。
文字不太好理解,看圖:
分配方式二:空閒列表
如果 Java 堆
中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒辦法簡單的進行指針碰撞了,虛擬機就必須維護一個列表
,記錄上哪些內存是可用的。在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄
,這種分配方式稱爲“空閒列表”
。
如何選擇?
選擇哪種分配方式由 Java 堆是否規整
決定,而 Java 堆是否規整又與所採用的垃圾收集器是否帶有壓縮整理功能
決定。因此,在使用 Serial
、ParNew
等帶 Compact
過程的收集器時,系統採用的分配算法是指針碰撞
,而使用 CMS
這種基於 Mark-Sweep 算法(標記 - 清除算法)
的收集器時,通常採用空閒列表
。
GC 標記 - 清除相關算法可參考:JVM 知識點整理:GC垃圾收集器及相關算法(標記 - 清除算法)
線程安全問題
除了空間劃分
外,對象創建在虛擬機中也是非常頻繁的行爲,即使是僅僅改變一個指針所指向的位置,在併發
情況下,也不是線程安全的
,可能出現正在給對象 A 分配內存,指針還沒來的及修改,對象 B 又同時使用了原來的指針分配內存的情況。
CAS + 失敗重試方法
CAS
操作包含三個操作數:內存位置(V)、預期原值(A)和新值(B)。
執行 CAS
操作的時候,將內存位置的值與預期原值比較
,如果相匹配
,那麼處理器會自動將該位置值更新爲新值
。否則,處理器不做任何操作。
處理器不做操作意思就是分配內存失敗
了,就重新獲取內存,通過執行相同的步驟重試
,直到成功爲止
。
線程本地分配緩存區(TLAB)
每個線程
在 Java 堆
中預先分配一小塊內存
,稱爲 線程本地分配緩存區(TLAB)
。哪個線程需要分配內存,就在哪個線程的 TLAB
上分配,只有在 TLAB
用完在分配新的內存時候,才需要同步鎖定
,虛擬機中是否採用 TLAB
,通過 -XX:+/-UseTLAB
參數來設定。
PS:就是每個線程在開始時候,優先在內存裏割一塊內存(劃地盤),要分配內存都優先在上面分配,不夠了再割塊內存出來(同步鎖定)
後續工作
內存分配完成後,虛擬機需要將分配的內存空間初始化爲零值(初始值)
(不包括對象頭)。這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就可以使用,程序能訪問這些字段對應的零值。
PS:設置初始值,如:int 類型默認值 0
接下來虛擬機對對象進行必要的設置,例如對象是哪個類的實例
、如何才能找到類的元數據信息
、對象的哈希碼
、對象的 GC 分代年齡
等信息,存放在對象頭(Object Header)中。
JVM
角度來看,新對象已經產生,但從 Java 程序角度來看,纔剛剛開始。後續需要執行 <init>(字節碼 class 文件指令)
,把對象按照程序員的意願進行初始化
,這樣一個真正可用的對象才完全產生。