JVM源碼分析-對象的創(chuàng)建過程
在開始MySQL的學(xué)習(xí)之前,還想寫一篇文章把前面學(xué)習(xí)的知識點回顧一下,就有了今天的這篇文章。
示例
有類School,這個類中有3個成員變量:引用類型String類型的schoolName,通過顯式代碼塊初始化;基本數(shù)據(jù)類型int型studentsNum,顯式初始化;引用類型Class類型student,通過School的構(gòu)造函數(shù)初始化。
我們使用main函數(shù)創(chuàng)建School的一個對象,那么這個過程發(fā)生了哪些事情?在JVM內(nèi)存中多了什么呢?讓我們一起看下吧!
- public class School {
 - private String schoolName;
 - private int studentsNum = 10000;
 - private Student student;
 - {
 - schoolName = "清華大學(xué)";
 - }
 - public School(){
 - student = new Student();
 - }
 - }
 - class Student{
 - }
 - class Test{
 - public static void main(String[] args) {
 - School school = new School();
 - }
 - }
 
當(dāng)我們執(zhí)行new School()時,進行了對象的創(chuàng)建,大致可以分為以下5步:
在詳細(xì)了解這5個步驟之前我們再詳細(xì)聊一下對象頭,在synchronized鎖升級過程分析的時候我們已經(jīng)初步接觸過它。
對象的內(nèi)存布局
對象在堆空間的內(nèi)存布局包含了3個部分:對象頭(Header)、實例數(shù)據(jù)(Intance Data)、對齊填充(Padding)。
對象頭
對象頭包含了兩部分:運行時元數(shù)據(jù)、指向類元數(shù)據(jù)的指針kclass,確認(rèn)這個對象所屬的類型。
運行時元數(shù)據(jù)(Mark Word)包含:哈希值、GC分代年齡、鎖狀態(tài)標(biāo)志位、偏向線程ID。運行時元數(shù)據(jù)的信息是變化的,在synchronized鎖的升級過程中,Mark Word在不同的鎖狀態(tài)下是不一樣的。
下圖展示展示了無鎖狀態(tài)、偏向鎖、輕量級鎖、重量鎖以及對象被GC標(biāo)記的對象頭中的運行時數(shù)據(jù)信息:
實例數(shù)據(jù)
實例數(shù)據(jù)是對象真正存儲的有效信息,它包含了對象中定義的各種類型的字段。這些字段有對象本身定義的,也有從所有父對象繼承的字段。
父類的構(gòu)造方法先于子類執(zhí)行,所以父類變量的定義都在子類前面。
對齊填充
對齊填充不是必須的,也沒有實在的意義,它僅僅是個占位符的作用。HotSpot虛擬機要求對象的起始地址必須是8字節(jié)的整數(shù)倍,因此當(dāng)對象沒有滿足的時候,就需要對齊填充來補全。
現(xiàn)在我們已經(jīng)了解了對象在堆內(nèi)存的布局,在之前的JVM文章中也學(xué)習(xí)了虛擬機棧結(jié)構(gòu)和方法區(qū)(JDK1.8之后稱為元空間,勾勾之前習(xí)慣稱為方法區(qū),但是怕大家混淆后續(xù)我們都用元空間表示),那么接下來我們詳細(xì)分析school對象創(chuàng)建的整個過程。
對象創(chuàng)建的步驟
對象的創(chuàng)建是在主線程的main()方法中,所以在主線程的虛擬機棧中就會創(chuàng)建main()的棧幀,main()就是當(dāng)前方法。
我們回顧下棧和棧幀。
JVM內(nèi)存區(qū)域劃分為5個模塊:堆、元空間、虛擬機棧、本地方法棧和程序計數(shù)器(也成為pc寄存器)。
虛擬機棧和本地方法棧都屬于棧,本地方法棧中只存放native方法的棧信息。
虛擬機棧的生命周期和線程的生命周期一致,它隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的銷毀而銷毀,所以它是線程私有的內(nèi)存區(qū)域。
虛擬機棧是由棧幀組成的,棧幀中包含了局部變量表、操作數(shù)棧、動態(tài)鏈接、方法返回地址、附加信息。棧幀是隨著方法的調(diào)用而創(chuàng)建的。所以當(dāng)主線程調(diào)用main()方法時,此時在主線程的虛擬機棧中就創(chuàng)建了main()棧幀。
main()棧幀中的局部變量表包含兩個變量:args和school。
主線程的虛擬機棧的棧幀結(jié)構(gòu)如下圖:
main()方法想要將school這個局部變量實例化,就需要執(zhí)行School這個類的實例化。
那么new School()發(fā)生了什么呢?我們接下來詳細(xì)分析之前的5個步驟。
判斷對象的類是否已經(jīng)加載
當(dāng)虛擬機遇到new這個指令時,會首先檢查這個指令的參數(shù)能否在元空間的常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載,即判斷元空間中是否包含這個類的類元信息。
我們通過javap -v -p Test.clas查看Test類的字節(jié)碼信息:
- Classfile /E:/study/javacodegirl/src/main/java/com/study/test/code/girl/base/jvm/Test.class
 - Last modified 2021-2-21; size 352 bytes
 - MD5 checksum 2df3d394ac88d2aa4da9d27f848067c5
 - Compiled from "School.java"
 - class com.study.test.code.girl.base.jvm.Test
 - minor version: 0
 - major version: 52
 - flags: ACC_SUPER
 - Constant pool:
 - #1 = Methodref #5.#14 // java/lang/Object."<init>":()V
 - #2 = Class #15 // com/study/test/code/girl/base/jvm/School
 - #3 = Methodref #2.#14 // com/study/test/code/girl/base/jvm/School."<init>":()V
 - #4 = Class #16 // com/study/test/code/girl/base/jvm/Test
 - #5 = Class #17 // java/lang/Object
 - #6 = Utf8 <init>
 - #7 = Utf8 ()V
 - #8 = Utf8 Code
 - #9 = Utf8 LineNumberTable
 - #10 = Utf8 main
 - #11 = Utf8 ([Ljava/lang/String;)V
 - #12 = Utf8 SourceFile
 - #13 = Utf8 School.java
 - #14 = NameAndType #6:#7 // "<init>":()V
 - #15 = Utf8 com/study/test/code/girl/base/jvm/School
 - #16 = Utf8 com/study/test/code/girl/base/jvm/Test
 - #17 = Utf8 java/lang/Object
 - {
 - com.study.test.code.girl.base.jvm.Test();
 - descriptor: ()V
 - flags:
 - Code:
 - stack=1, locals=1, args_size=1
 - 0: aload_0
 - 1: invokespecial #1 // Method java/lang/Object."<init>":()V
 - 4: return
 - LineNumberTable:
 - line 28: 0
 - public static void main(java.lang.String[]);
 - descriptor: ([Ljava/lang/String;)V
 - flags: ACC_PUBLIC, ACC_STATIC
 - Code:
 - stack=2, locals=2, args_size=1
 - 0: new #2 // class com/study/test/code/girl/base/jvm/School
 - 3: dup
 - 4: invokespecial #3 // Method com/study/test/code/girl/base/jvm/School."<init>":()V
 - 7: astore_1
 - 8: return
 - LineNumberTable:
 - line 30: 0
 - line 31: 8
 - }
 - SourceFile: "School.java"
 
在main()中new指令的參數(shù)是#2,我們可以在Constant pool中找到#2對應(yīng)的類信息。
如果沒有這個類的信息,那么就會按照雙親委派模型加載School類。
類的加載過程:加載、連接、初始化,其中連接包括:驗證、準(zhǔn)備、解析。
執(zhí)行類的加載的是類加載器,它分為:啟動類加載器、擴展類加載器、應(yīng)用類加載器和自定義加載器。
School類是ClassPath下的文件,它的類加載是應(yīng)用類加載器,當(dāng)應(yīng)用類加載器按照ClassLoader+包名+類名查找對應(yīng)的.class文件時,如果找不到這個文件就會拋出ClassNotFoundException異常,如果找到了則進行類的加載,并生成對應(yīng)的Class類對象。這個時候在元空間中就有了School的類元數(shù)據(jù)了。
為對象分配內(nèi)存空間
接下來就需要計算對象占用的空間大小,基本類型除了long和double是8個字節(jié),byte和boolean是1個字節(jié),char和short是2個字節(jié),其他基本類型都是4個字節(jié),引用類型也是4個字節(jié)。
內(nèi)存大小計算好之后在堆中劃分一塊內(nèi)存空間給新對象。大部分情況下,對象是在新生代的Eden區(qū)中分配,如果此時Eden區(qū)沒有足夠的內(nèi)存空間進行分配,虛擬機將發(fā)起一次Minor GC。但是當(dāng)我們?yōu)橐粋€很長的字符串或者數(shù)組分配內(nèi)存時,這種類型的大對象需要連續(xù)的內(nèi)存空間,可以直接在老年代進行分配,這樣做可以避免Eden和兩個S區(qū)發(fā)生大量的內(nèi)存復(fù)制。但是大對象可能會導(dǎo)致連續(xù)空間不足而提前觸發(fā)GC,我們開發(fā)中也應(yīng)該盡量避免大對象。
內(nèi)存分配有兩種方式:指針碰撞和空閑列表分配。
- 指針碰撞:當(dāng)內(nèi)存使用的GC算法是標(biāo)記整理或者復(fù)制算法時,內(nèi)存是規(guī)整的,此時我們?yōu)閷ο蠓峙鋬?nèi)存只需要移動指針位置就可以。Serial和ParNew使用的GC回收算法是標(biāo)記復(fù)制算法,內(nèi)存的分配就是指針碰撞的方式。
 
- 空閑列表分配:當(dāng)內(nèi)存使用的GC算法是標(biāo)記清除算法時,內(nèi)存是規(guī)整的,這個時候維護了內(nèi)存空閑的列表,在為新對象分配內(nèi)存時從空閑列表中找到內(nèi)存就可以。CMS使用的GC回收算法是標(biāo)記清除算法,內(nèi)存的分配方式就是空閑列表分配。
 
看完內(nèi)存的分配你有沒有疑問?堆內(nèi)存是所有線程共享的,如果兩個線程同時都想占用這一塊內(nèi)存空間怎么辦呢?這就涉及到了分配內(nèi)存空間時的并發(fā)安全問題。
JVM提供了兩種處理并發(fā)安全的方式:一種是我們常用的CAS失敗重試+區(qū)域鎖來保證內(nèi)存分配的原子性,另外一種是通過開啟-XX:+UseTLAB參數(shù)為每個線程預(yù)分配一塊TLAB,在JDK1.8中這個參數(shù)是默認(rèn)開啟的。
經(jīng)過了這一步之后,堆內(nèi)存中就有了School實例的一塊內(nèi)存區(qū)域了:
初始化分配到的內(nèi)存空間
屬性的賦值操作分為3個類型,我們在示例中都有舉例:
- 默認(rèn)值初始化
 - 顯式初始化和代碼塊初始化
 - 構(gòu)造方法初始化
 
初始化分配到的內(nèi)存空間是默認(rèn)值初始化,它為類的成員變量設(shè)置默認(rèn)值,保證對象實例字段在不賦值時可以直接使用?;緮?shù)據(jù)類型的默認(rèn)值為0,布爾類型的默認(rèn)值為false,引用類型的默認(rèn)值為null。
不要把這一步的初始化和類加載過程中的初始化混淆了!
類加載過程中的初始化是對類的靜態(tài)變量初始化,不包含類的實例變量。
執(zhí)行了這一步之后,內(nèi)存中的情況如下圖:
設(shè)置對象的對象頭
將對象的所屬的類、對象的HashCode值、對象的GC信息、鎖信息等數(shù)據(jù)存放在對象頭中。它取決于JVM實現(xiàn)。對象頭的信息我們前面已經(jīng)講過,這里不再贅述。
執(zhí)行了這一步之后內(nèi)存中的數(shù)據(jù)變化:
執(zhí)行init進行初始化
這個時候初始化過程才真正開始。這個過程是對應(yīng)字節(jié)碼invokespecial,執(zhí)行init方法。
它會執(zhí)行實例化代碼塊、調(diào)用類的構(gòu)造方法、將堆內(nèi)對象的首地址賦值給引用變量。這一步之后真正可用的對象才算創(chuàng)建完成。
執(zhí)行了這一步之后內(nèi)存中的變化如下圖:
總結(jié)
對象的創(chuàng)建過程:類元數(shù)據(jù)加載->分配內(nèi)存空間并解決并發(fā)問題->初始化分配的內(nèi)存空間->設(shè)置對象頭信息->執(zhí)行init方法進行初始化。
對象的整個創(chuàng)建過程大家要對JVM的內(nèi)存區(qū)域比較了解,熟悉每個區(qū)域存放的數(shù)據(jù),并知道在哪個過程存的數(shù)據(jù)。
類元數(shù)據(jù)的加載是元空間的數(shù)據(jù)來源,我們還可以回顧下類加載機制、雙親委派模型、哪些場景下需要打破雙親委派,之前勾勾分析了JDBC的SPI機制,利用線程上下文類加載器打破雙親委派。
對象的創(chuàng)建都是基于堆空間的,我們可以回顧下堆空間的內(nèi)存分配、GC回收算法和GC回收器。
設(shè)置對象頭信息我們需要了解對象頭,還可以按照對象頭的數(shù)據(jù)變化回顧synchronized鎖的升級過程。
對象創(chuàng)建之后內(nèi)存的數(shù)據(jù)變化如下圖:


























 
 
 








 
 
 
 