對象的創建
1.類加載檢查
虛擬機遇到一條new的指令,首先去檢查這個指令的參數能否在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行類的加載過程。
2.分配內存
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存,對象所需內存在類加載之后便可確定,為對象分配空間的任務等于說從把一塊確定大小的內存從Java堆中劃分出來。分配方式有指針碰撞和空閑列表兩種方式,選擇哪種方式由Java堆是否規整決定,而 Java 堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種方式:
指針碰撞:
適用場景:堆內存規整的情況下(沒有內存碎片)。
原理:用過內存的整合到一邊,沒有用過內存的在另一邊,中間是分界指針,指針向著沒有用過內存的一邊移動對象內存大小位置即可。
空閑列表:
適用場景:堆內存不規整的情況下。
原理:虛擬機會維護一個列表,記錄哪些內存塊是可用的,分配空間時,找一塊內存大小足夠的內存塊來劃分給對象示例,最后更新列表。
內存分配并發問題:
在創建對象時,一個很大的問題就是線程安全,因為在并發場景下,創建對象十分頻繁,作為虛擬機來說,必須保證線程安全,虛擬機有兩種方法來保證線程安全:
CAS+失敗重試:CAS是樂觀鎖實現的一種,如果不了解CAS和樂觀鎖的可以看我之前寫的關于樂觀鎖的博客:【并發編程 | 第四篇】悲觀鎖與樂觀鎖的學習-CSDN博客,所謂樂觀鎖就是,每次操作都不加鎖,而是假設沒有沖突去完成某個操作,如果失敗,就重試直到成功為止。虛擬機利用CAS+重試的方式更新操作的原子性。
TLAB:為每一個線程預先在 Eden 區分配一塊兒內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大于 TLAB 中的剩余內存或 TLAB 的內存已用盡時,再采用上述的 CAS 進行內存分配。
3.初始化零值
內存分配完成后,虛擬機需要將分配到的內存空間都初始化零值,這一步操作保證了對象的示例在Java代碼執行時不需要賦值就可直接使用,程序能訪問到這些字段的數據類型所對應的初始值。
4.設置對象頭
初始化零值完成之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
5.執行init方法
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛開始,<init>
方法還沒有執行,所有的字段都還為零。所以一般來說,執行 new 指令之后會接著執行 <init>
方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
對象的內存布局
在HotSpot虛擬機中,對象在內存中的布局可以分為三塊區域:對象頭、實例數據、對齊填充。
對象頭包含兩部分信息:
1.標記字段:用于存儲對象自身運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等等。
2.類型指針:對象指向它的類元數據指針,虛擬機通過這個指針來確定它是哪個類的示例對象。
實例數據部分是對象真正存儲的有效信息,也就是在程序中所定義的各種字段的信息。
對齊填充部分不是必然存在的,也沒有什么特別的含義,僅僅起占位作用。因為 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
如果我的內容對你有幫助,請點贊,評論,收藏。創作不易,大家的支持就是我堅持下去的動力!