使用new關鍵字創建對象是一個什么過程呢?
1.首先將去檢查這個指令的參數是否能在常量池中定位到一個類的引用,并且檢查這個符號引用代表的類是否已被加載、解析、初始化。如果沒有,把必須先執行相應的類加載過程。
2.在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務實際上便等同于把一塊確定大小的內存塊從Java堆中劃分出來。
“指針碰撞”:假設Java堆中內存時絕對規整的,所有被使用過的內存都被放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅把那個指針向空閑空間方向挪動一段與對象大小相等的距離,這種分配方式成為“碰撞指針”。
“空閑列表”:但如果Java堆中的內存不是規整的,已被使用的內存和空閑的內存交錯在一起,那就沒有辦法簡單的進行指針碰撞,虛擬機就必須維護一個列表,上面記錄著哪些內存塊是可用的,在分配內存時從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”。
選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有空間壓縮整理的能力決定。因此,當使用Serial、ParNew等帶壓縮整理過程的收集器時,系統采用的分配算法是指針碰撞,既簡單又高效;而當使用CMS這種基于清除算法的收集器時,理論上就只能采用較為復雜的空閑列表來分配內存。
除了如何劃分可用空間之外,還有另外一個需要考慮的問題:對象創建是非常頻繁的行為,即使僅僅修改一個指針所指向的位置,在并發情況下也不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。
解決這個問題有兩種可選方案:
一:對分配內存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性;
二:把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),哪個線程要分配內存就在哪個線程的本地緩沖區中分配,只有本地緩存區用完了,分配新的緩沖區時才需要同步。
3.內存分配完成后,虛擬機必須還要將分配到的內存空間(不包括對象頭)都初始化為零,如果使用了TLAB的話,這一項工作也可以提前志TLAB分配時順便進行。這步操作保證了對象的實例字段在Java代碼中可以不賦初始值就能使用,使得程序訪問到這些字段的數據類型所對應的零值。
4.接下來,Java虛擬機還要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希值(真正調用Object::hashCode()時才計算)、對象的GC分代年齡等信息。這些信息存放在對象的對象頭。根據虛擬機狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。對象頭的具體內容,以后介紹。
5.上面的工作完成后,從虛擬機的視角看,一個新的對象已經產生了。但從從Java程序的視角看來,對象創建才剛剛開始——構造函數,即Class文件中的<init>()方法還沒有執行,所有的字段都為默認的零值,對象需要的其他資源和狀態信息還沒有按照預定的意圖構建好。new指令之后接著會執行<init>()方法,按照程序員的意愿對對象進行初始化,這樣一個真正可用的對象才算完全被構造出來。
碼字不易,轉載請標明原文出處。
摘自《深入理解Java虛擬機》第三版 周志明