JVM 如何建立Java物件
在Java程式中,建立一個物件通常需要一個new關鍵字就夠了,但是在虛擬機器中,這個過程卻有點複雜,這裡麵包括了類載入、記憶體分配、初始化零值等等一系列的步驟。
下面來看看JVM如何建立一個物件(這裡面的物件僅僅限於不同的Java物件,不包括陣列和Class物件)
1 物件的建立
1.1 類初始化
當JVM遇到一條new的指令(與new關鍵字不是一個概念)時,首先去檢查這個指令是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經載入、解析和初始化過。如果沒有,那麼必須先執行類的初始化工作。
1.2 劃分空間
接下來就是要堆中劃分出一塊空間,這塊空間的大小由類去確定,在類載入以後,一個物件的大小已經是確定的了。對於這個劃分,可能存在兩種情況
(1)堆的空間是規整的,已用過的部分在一邊,未用過的部分在另外一邊,有一個指標指向未使用部分的頭,每次移動這個指標就可以了,這種方法稱為“指標碰撞”。
(2) 堆的空間是零散的,使用和未使用的部分交叉排列,這時候就需要一個維護一個列表,記錄哪些記憶體是可用的,在分配的時候找到一塊足夠大的記憶體進行分配,這種方法稱為“空閒列表”。很顯然這種方式會產生很多記憶體碎片。
實際中採用哪種分配方式是由虛擬機器採用的垃圾收集演算法決定的,主要取決於垃圾收集器是否帶有壓縮整理功能(campact)。因此,在使用Serial,ParNew等待Compact過程的收集器時,系統採用指標碰撞,而使用CMS這種基於Mark-Sweep演算法的收集器時,採用空閒列表。
如何保證物件建立的執行緒安全性?
物件建立是虛擬機器中頻繁發生的行為,移動指標時如何保證執行緒安全呢?這個問題有兩個解決方法
1 採用CAS+失敗重試保證更新操作證的原子性
2 把記憶體分配動作按照執行緒劃分在不同空間中進行。在堆中為每個記憶體分配一小塊空間,稱為本地執行緒分配緩衝(TLAB)。執行緒分配記憶體時,在自己的TLAB上分配,當TLAB使用完時,使用同步鎖。
1.3 賦零值
記憶體分配完成後,虛擬機器把分配到的記憶體空間初始化為零值,如果使用TLAB,這一過程在TLAB中進行。這一步保證了Java的例項欄位在Java程式碼中不同賦值就可以使用
1.4 設定物件頭
虛擬機器啊要對物件進行必要的設定,例如物件時哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊儲存在物件頭中。
1.5 <init>指令
在上面的工作完成後,對於虛擬機器來說,一個物件已經建立完成,帶從Java物件的角度,還沒有進行初始化,<init>方法還沒有執行,所有的欄位都還是零值。這個<init>方法可以理解為物件的構造方法
2 物件的記憶體佈局
在HotSpot虛擬機器中,物件在記憶體中儲存的佈局分為3個部分:物件頭(Header)、例項資料(Instance Data) 和對其填充
2.1 物件頭
物件頭包括兩個部分,第一個部分用於儲存物件自身的執行時資料,這個部分的長度在32位和64位的虛擬機器中分別為32bit和64bit,官方稱為“Mark Word”。物件頭被設計稱為與物件結構無關的一個數據結構,32bit的儲存內容如下:
儲存內容 | 標誌位 | 狀態 |
物件雜湊碼、物件分代資訊 | 01 | 未鎖定 |
指向鎖記錄的指標 | 00 | 輕量級鎖定 |
指向重量鎖的指標 | 10 | 重量級鎖定 |
空,不需要記錄i資訊 | 11 | GC標記 |
偏向執行緒ID、偏向時間戳、物件分代資訊 | 01 | 可偏向 |
2.2 例項資料域
這個部分是物件真正儲存的有效資訊,也就是程式程式碼中所定義的各種型別的欄位內容。無論是從父類繼承下來的,還是子類中定義的,都需要記錄起來。這部分的儲存順序受到虛擬機器分配策略引數和欄位在Java原始碼中定義的順序的影響。HotSpot虛擬機器預設的分配策略為long/doubles、ints、shorts/chars,bytes/booleans、oops,從分配策略來看,相同寬度的欄位總是被分配在一起。在滿足這個前提下,父類的變數會出現在子類之前。如果指定了compactFileds引數為true,那麼子類之中較窄的變數可能會插入到父類變數的空隙之中。
2.3對齊填充
這個部分並不一定有,而且沒有什麼實際意義,僅僅是為了填充資料。HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,也就是每個物件佔得大小必須是8位元組的整數倍,如果例項域不滿足,就要補充對其。