在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字節的整數倍,如果實例域不滿足,就要補充對其。