對象創建流程
我們知道JVM三大組成部分: 類加載子系統、運行時數據區 、字節碼執行引擎。
要想new 一個對象,肯定是要繞不開JVM的機制。
【類加載檢查】
JVM啓動的時候並不是將所有的類都初始化,所以當碰到一個new指令時,JVM首先會去檢查這個類有沒有被加載,具體就是去常量池中看是否有這個類的符號引用,並檢查這個符號引用代表的類是否已經被加載、解析和初始化過 。 若沒有這必須經歷【類加載子系統】的歷練 (加載–校驗–準備–解析–初始化)
【分配內存】
類加載校驗通過後 ,是不是該分配內存了呢?
是的, 接下來JVM將會爲這個新生的對象分給內存,因爲這個新生對象所需要內存大小在類加載完之後便可以完全確定,對象放哪裏呢? 通常都是放在堆中,所以所謂的分配內存實際上就是從Java堆中劃分出一塊固定大小的內存給這個新生對象。
雖然很簡單的一件事情,但是要考慮的地方可不少
- 採取何種方式分配內存
- 併發問題
內存劃分的兩種方式
JVM提供了2中劃分內存的方法
- 指針碰撞(Bump the Pointer) 【默認方式】
如果堆中的內存是絕對規整的,大家都按順序排放,分配過內存的對象那個在一邊,未使用的內存在另外一邊 ,分界線使用指針來維護。因爲新生對象所需要內存大小在類加載完之後便可以完全確定,所以僅需要將指針移動對象大小的位置即可。
當然了這是一種理想的情況,JVM裏還有GC,會標記清除等等
-
空閒列表(Free List)
如果堆內存中的內存並不是規整的,分配的內存和未分配的內存糅雜在一起, 如果還用上面的指針碰撞的方式, 如果移動的可用內存無法容納這個對象,放不下啊? 咋弄? 繼續碰麼?
顯然效率很低。 所以JVM採用了另外一種方式,JVM維護了一個列表,記錄了堆中的可用內存,那麼分配內存的時候就從JVM維護的列表中找一個足夠容納這個對象的內存區域給它,並更新列表記錄。
解決分配內存併發問題的兩種方式
第二個問題 併發問題如何解決呢?
在併發的情況下,可能出現JVM正在給對象A分配內存,但是指針還沒來得及修改,對象B又使用了A的內存空間的情況。
爲了解決這個問題,JVM採取了
- CAS (compare and swap)
簡而言之就是JVM採用【 CAS+失敗重試 】保證更新操作的原子性 。
- 本地線程分配緩衝 (Thread Local Allocation Buffer , TLAB)
把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存
通過XX:+/ UseTLAB
參數來設定虛擬機是否使用TLAB。
JDK8中默認開啓XX:+UseTLAB
,默認值eden區域的1%,當然了也可以通過-XX:TLABSize
指定TLAB大小 。 一般不建議修改。
如果TLAB還放不下,那就走CAS了…
不管怎麼分配,目的只是爲了更好的回收內存或者更快的分配對象
【初始化】
內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭).
如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。
這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的默認值 (比如 int 默認0 , String 默認null , boolean 默認false等等)
【設置對象頭】
初始化默認值以後,JVM要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭Object Header之中。
這部分數據的長度在32位和64位的虛擬機中分別爲32個和64個bits,官方稱它爲“Mark Word”。
對象的組成
在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、 實例數據(Instance Data)和和對齊填充(Padding) 。
對象頭的兩部分組成
HotSpot虛擬機的對象頭包括兩部分信息
- 第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時 間戳等。
32位操作系統爲例
- 對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
如下所示
【執行init方法】
執行方法,即對象按照程序員的意願進行初始化。對應到語言層面上講,就是爲屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值) 和執行構造方法。
IDEA安裝jclasslib插件可以查看
這裏的init實際上是C++調用的,相對於面向開發人員 就是 new Artisan() ,並執行Artisan默認的構造函數。