一個Java 象的建立過程
在前面我們已經瞭解過JVM 的記憶體劃分,我們知道一個Java 物件(文中討論的限於普通Java 物件)在Java 堆上進行分配,下面我們就以HotSpot 虛擬機器為例聊聊一個Java 象的建立。
物件的建立過程
當虛擬機器遇到一個new 指令時,會經過以下幾個步驟:
1. 判斷是否載入類
首先JVM 會去檢查這個指令的引數能否在方法區的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
2. 分配記憶體空間
2.1 分配記憶體空間的兩種方式
類載入完成後,虛擬機器會為新生物件分配記憶體。物件所需的記憶體在類載入後便可完全確定。分配方法分為兩種:
指標碰撞(Bump the Pointer)
假設Java 堆的記憶體是絕對規整的,用過的記憶體放在一起,沒用的記憶體放在一起(強迫症患者喜歡的那種),中間放著一個指標作為分界點的指示器,物件需要多少記憶體就把指標向空閒記憶體空間移動多少。這個方式有點像一個鐘:一天的24小時就是虛擬機器總記憶體,用過的記憶體就是時針走過的時間,沒走過的就是剩下的空閒記憶體。分配一個物件就像吃飯花了一小時,指標就往空閒的地方走1格。
空閒列表(Free List)的分配:
假設Java 堆的記憶體空間不是規整的,已使用的和未使用的記憶體空間冗雜在一起,虛擬機器就會維護一張列表來記錄空間的使用情況,記錄那些空間是可用的,那些空間是不可用的。分配物件的時候,就在改表中找到一塊足夠大的記憶體空間劃分給物件例項。
選擇哪種方式分配物件由Java 堆是否規整決定,而Java 堆是否規整由所採用的垃圾收集器(關於垃圾收集器後面在講)是否帶有壓縮整理功能決定。
簡單列列:
指標碰撞:Serial、ParNew等帶Compact過程的收集器
空閒列表:CMS 基於Mark-Sweep 演算法的收集器
2.2 分配的併發問題解決
另一個需要考慮的問題就是物件建立在虛擬機器是否是頻繁的。在併發情況下,哪怕是移動指標這個簡單的動作也可能出現執行緒不安全的問題。可能出現執行緒A 分配了物件還沒來得及移動指標,執行緒B 又使用原來的指標來分配記憶體的情況。如何解決?
- 同步方案
虛擬機器通過對分配記憶體空間的動作採用CAS 配上失敗重試的方式保證記憶體更新操作的原子性。 - 非同步方案
非同步方案就是把記憶體分配的動作按照執行緒劃分在不同的空間中進行,即每個執行緒在Java 堆中預先分配一小塊記憶體,稱為本執行緒分配緩衝(Thread Local Allocation Buffer,TLAB)。只有在本地執行緒分配完了並分配新的TLAB 時,才需要同步鎖定。
3. 記憶體空間初始化零值
記憶體分配完成後,虛擬機器需要將分配到的記憶體空間初始化為零值(不包括物件頭),如果使用TLAB ,這一工作也可以提前至TLAB 分配時進行。這一步操作保證了物件的例項欄位在Java 程式碼中可以不賦值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
4. 物件設定
在記憶體空間初始化後,虛擬機器要對物件進行必要的設定,例如這個物件是那個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC 分代年齡等資訊。
參考資料:
《深入理解Java虛擬機器》