2.3.1 Java 對象的創建過程(五步)
下圖便是 Java 對象的創建過程,我建議最好是能默寫出來,並且要掌握每一步在做什么
Step1:類加載檢查
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
Step2:分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需的內存大小在類加載完成后便可確定,為對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中划分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇哪種分配方式由 Java 堆是否規整決定,而 Java 堆內存是否規整取決於 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制算法內存gai也是規整的
內存分配方式
內存分配並發問題
在創建對象的時候有一個很重要的問題,就是線程安全,因為在實際開發過程中,創建對象是很頻繁的事情,作為虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機采用兩種方式來保證線程安全:
- CAS+失敗重試: 對分配內存空間的動作進行同步處理,實際采用的是CAS+失敗重試來保證更新操作的原子性。CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
- TLAB(Thread Local Allocation Buffer,本地線程分配緩沖):把內存分配的動作按照線程划分在不同的空間中進行,即每個線程在Java堆中預先分配一小塊內存,哪個線程要分配內存,就在哪個線程的本地緩沖區進行分配,只有本地緩沖區用完了,再采用同步鎖定的方式為線程分配新的緩沖區。
Step3:初始化零值
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
Step4:進行必要設置,比如對象頭
初始化零值完成之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
Step5:執行 init 方法
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始——構造函數,即class文件中的<init>
方法還沒有執行,所有的字段都還為零,對象需要的其他資源和狀態信息還沒有按照預定的意圖構造好。所以一般來說,執行 new 指令之后會接着執行 <init>
方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全被構造出來。
2.3.2 對象的內存布局
考慮到虛擬機的空間效率,其中Mark Word 被設計成一個具有動態定義的數據結構,以便在極小的空間內存儲盡量多的數據,根據對象的狀態復用自己的存儲空間。
2.3.3 對象的訪問定位
建立對象就是為了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有使用句柄和直接指針兩種:
句柄:如果使用句柄的話,那么 Java 堆中將會划分出一塊內存來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;
直接指針:如果使用直接指針訪問,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。
這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時(垃圾收集時移動對象是非常普遍的行為)只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷,而對象訪問是非常頻繁的,因此開銷可觀。HotSpot 采用的就是第二種直接指針的方式。