有興趣可以先參考前面的幾篇JVM總結(jié):
JVM自動內(nèi)存管理機(jī)制-Java內(nèi)存區(qū)域(上)
JVM自動內(nèi)存管理機(jī)制-Java內(nèi)存區(qū)域(下)
我們知道,在編寫一個Java程序后,需要由虛擬機(jī)將描述類的數(shù)據(jù)從Class文件(這里面的Class文件不是指某個特定存在于磁盤上面的文件,而是一串二進(jìn)制字節(jié)流)加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可被虛擬機(jī)使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。與編譯時需要進(jìn)行連接工作的語言不同,Java中類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的,雖然會有一些性能的開銷但是也為其提供了比較高的靈活性,Java中可以動態(tài)擴(kuò)展和依賴運(yùn)行期動態(tài)加載和動態(tài)連接就是依賴于這種特點(diǎn)的。
一、類加載概述
1、類加載過程概述
類從被夾在到虛擬機(jī)內(nèi)存中開始,到被卸載出內(nèi)存的整個生命周期大概包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)。這其中的加載、驗(yàn)證、準(zhǔn)備、初始化和卸載5個階段的順序是一定的,類的加載過程必須要按照這幾個順序來開始(這些不同的階段可能是混合的交叉執(zhí)行,可能再一個階段執(zhí)行的時候激活另一個階段)。
而解析階段不同的特點(diǎn)就是:為了支持Java的運(yùn)行時綁定(動態(tài)綁定或者晚期綁定),在某些情況下可以在初始化之后再執(zhí)行。
2、初始化階段的5中情況(必須對類進(jìn)行初始化)
a)遇到new、getstatic、putstatic、invokstatic這四條字節(jié)碼指令的時候,如果類沒有進(jìn)行初始化,那么必須要對類觸發(fā)其初始化。典型場景
?、偈褂胣ew實(shí)例化對象的時候;
②讀取或者設(shè)置一個類的靜態(tài)字段(除開被final修飾、編譯器將結(jié)果放入常量池的靜態(tài)字段);
③調(diào)用一個類的靜態(tài)方法的時候;
b)使用反射包的時候(java.lang.reflect),使用其中的方法進(jìn)行反射調(diào)用時必須對類觸發(fā)其初始化(如果類沒有被初始化過)
c)當(dāng)初始化一個類的時候,其父類如果還沒有被初始化過,那么必須先觸發(fā)器父類的初始化
d)當(dāng)用戶在虛擬機(jī)啟動時候指定需要執(zhí)行的主類(包含main()方法的那個類),會首先初始化這個類
e)使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實(shí)例的最后解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則要先觸發(fā)其初始化。?
3、不會執(zhí)行初始化的幾種情況
?、偻ㄟ^子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化(后面有示例程序)。
?、诙x對象數(shù)組,不會觸發(fā)該類的初始化。
③常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類。
?、芡ㄟ^類名獲取Class對象,不會觸發(fā)類的初始化。
?、萃ㄟ^Class.forName加載指定類時,如果指定參數(shù)initialize為false時,也不會觸發(fā)類初始化,其實(shí)這個參數(shù)是告訴虛擬機(jī),是否要對類進(jìn)行初始化。
?、尥ㄟ^ClassLoader默認(rèn)的loadClass方法,也不會觸發(fā)初始化動作。
4、主動引用和被動引用
a)主動引用:上面的5中情況中的場景會觸發(fā)類進(jìn)行初始化,這些行為被稱為對一個類進(jìn)行主動引用
b)被動引用:所引用類的方式不會被初始化。下面介紹集中被動引用的例子以及測試代碼
?、偻ㄟ^子類引用父類的靜態(tài)字段,只會初始化父類,子類不會被初始化。在下面的例子中,子類繼承自父類,但是輸出的時候只會輸出“父類被初始化”,而沒有“子類被初始化”。
1package cn.jvm.classLoad; 2 3class SuperClass { 4static { 5System.out.println("父類被初始化"); 6? ? } 7 8publicstaticinttest = 666; 9}1011classSubClassextends SuperClass {12static {13System.out.println("子類被初始化");14? ? }15}1617publicclass TestClass1 {18publicstaticvoid main(String[] args) {19? ? ? ? System.out.println(SubClass.test);20? ? }21}
總結(jié)來說就是:對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,索引通過子類引用父類中定義的靜態(tài)字段只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。
?、谕ㄟ^數(shù)組定義來引用類,不會觸發(fā)該類的初始化。下面的測試程序也很明顯,運(yùn)行之后不會輸出“父類被初始化”
1package cn.jvm.classLoad; 2class SuperClass1 { 3static { 4System.out.println("父類被初始化"); 5? ? } 6 7publicstaticinttest = 666; 8} 9publicclass TestClass2 {1011publicstaticvoid main(String[] args) {12SuperClass[] sc =newSuperClass[6];13? ? }14}
當(dāng)初始化對象數(shù)組時,并不會實(shí)際觸發(fā)對象的初始化操作。但是會觸發(fā)一個是由虛擬機(jī)自動生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動作由字節(jié)碼指令newarray觸發(fā)。值得注意的是:該類代表了實(shí)際的對象數(shù)組,數(shù)組中應(yīng)有的方法和屬性都實(shí)現(xiàn)在這個類里。
?、鄢A吭诰幾g階段就會存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量類的初始化。如同下面的測試程序,只會輸出定義的常量字符串,而不會輸出“定義常量的Test類被初始化”。
雖然在TestClass3類中引用了Test類的常量test,但是在編譯階段經(jīng)過常量傳播優(yōu)化,將常量的值存到了TestClass3類的常量池中,以后再使用test的引用實(shí)際上都是轉(zhuǎn)換為TestClass3類對自身常量池的引用
1package cn.jvm.classLoad; 2 3class Test { 4static { 5System.out.println("定義常量的Test類被初始化"); 6? ? } 7publicstaticfinalString test = "TestClass"; 8} 910publicclass TestClass3 {1112publicstaticvoid main(String[] args) {13? ? ? ? System.out.println(Test.test);14? ? }15}
?、蕻?dāng)一個常量的值并非在編譯期間可以確定的,那么其值就不會被放到調(diào)用類的常量池中。這個時候在程序運(yùn)行時,會導(dǎo)致主動使用這個常量所在的類,即會觸發(fā)這個類的初始化。
5、關(guān)于接口的初始化
a)接口也有自己的初始化過程:接口中沒有static靜態(tài)代碼塊來輸出初始化信息,編譯器會為接口生成“<clinit>()”類構(gòu)造器,用于初始化接口中所定義的成員變量。?
b)接口和類初始化的區(qū)別:當(dāng)一個類在初始化時,其父類都基本上初始化過了,然而接口在初始化的時候,只有真正用到父接口的時候(如引用接口中定義的常量)才會進(jìn)行初始化。
二、類加載全過程
1、加載
a)在加載階段虛擬機(jī)通過一個類的限定名來獲取定義此類(某個Class文件)的二進(jìn)制字節(jié)流,然后將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu),之后在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
?、賹τ诜菙?shù)組類的加載階段中獲取類的二進(jìn)制字節(jié)流的動作,是可控性最強(qiáng)的。因?yàn)樵诩虞d階段中既可以使用系統(tǒng)提供的引導(dǎo)類加載器完成類加載,也可以由用戶自定義的類加載器完成,開發(fā)人云通過定義自己的類加載器去控制字節(jié)流的獲取方式(重寫一個累加器的LoadClass方法,后面會有例子程序作為示例)
②對于數(shù)組類而言,由于數(shù)組類本身不通過類加載器創(chuàng)建,而是有JVM直接創(chuàng)建的,但是數(shù)組類與類加載器仍然有比較密切的關(guān)系(數(shù)組類的元素類型最終是需要通過類加載器去進(jìn)行加載的)
2、連接階段(分為驗(yàn)證,準(zhǔn)備和解析三個階段)
a)驗(yàn)證
驗(yàn)證是連接階段的第一步,這一步的目的是為了確保Class文件的字節(jié)流包含的信息對于虛擬機(jī)是安全的。驗(yàn)證階段大概會包括四個小的檢驗(yàn)動作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號引用驗(yàn)證。
①文件格式驗(yàn)證:這一階段的驗(yàn)證主要指驗(yàn)證字節(jié)流是否符合Class文件的規(guī)范,并且能夠被當(dāng)前版本的虛擬機(jī)處理(魔數(shù)0XCAFEBABE開頭、主次版本號是否在虛擬機(jī)處理范圍之內(nèi)、常量池中常量是否有不被支持的常量類型等等));
②元數(shù)據(jù)驗(yàn)證:這個階段是對字節(jié)碼的信息進(jìn)行語義描述,保證描述的信息符合Java語言規(guī)范(除了java.lang.Object類之外的類是否有父類、這個類是否繼承了不應(yīng)被繼承的類、如果這個類不是抽象類是否實(shí)現(xiàn)類其父類或者接口中的所有方法);
③字節(jié)碼驗(yàn)證:通過數(shù)據(jù)流和控制流進(jìn)行分析,確定程序語義是合法的、符合邏輯的,在元數(shù)據(jù)驗(yàn)證完畢之后這個階段對類的方法進(jìn)行驗(yàn)證,保證類的方法運(yùn)行時是對虛擬機(jī)安全的;
④符號引用驗(yàn)證:該階段發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)換為直接引用的時候,這個轉(zhuǎn)換的動作會發(fā)生在連接的第三個階段(解析),符號引用驗(yàn)證可以看做是對類自身之外的信息進(jìn)行匹配性校驗(yàn)(符號引用中字符串描述的全限定名是否能夠找到對應(yīng)的類;在指定類中是否存在方法的字段描述符以及簡單名稱所描述的方法和字段;符號引用中的類、字段、方法的訪問權(quán)限(private、protected、public、default)是否可以被當(dāng)前類訪問)
b)準(zhǔn)備
準(zhǔn)備階段是為類變量分配內(nèi)存并設(shè)置類的初始值(通常情況下指的是數(shù)據(jù)類型的默認(rèn)零值,如果被是常量值比如public static final int test = 123,這就會在準(zhǔn)備階段將變量test初始化為123)的階段,這些變量所使用的內(nèi)存都會在方法區(qū)中進(jìn)行分配。
注意:
?、龠@里進(jìn)行內(nèi)存分配的變量只包括類變量(static變量),而不包括實(shí)例化變量,實(shí)例變量將會在對象實(shí)例化的時候隨著對象一起分配在堆中;
?、诔跏贾担ㄍǔG闆r下指的是數(shù)據(jù)類型的默認(rèn)零值,如果被是常量值比如public static final int test = 123,這就會在準(zhǔn)備階段將變量test初始化為123)
c)解析
? 解析階段是虛擬機(jī)將常量池中的符號引用替換為直接引用的過程,符號引用在Class文件中以CONSTANT_Class_info、CONSTANT_Field_info等類型的變量出現(xiàn)。
①符號引用:是以一組符號來描述所引用的目標(biāo),可以使任何形式的字面量(使用時候沒有歧義的量)
②直接引用:可以直接指向目標(biāo)的指針、相對偏移量或者是一個能夠間接定位到目標(biāo)的句柄
3、初始化階段
初始化階段是類加載過程的最后一個階段,在前面基本上都是類加載器參與執(zhí)行(包括自定義的類加載器),在初始化階段才是執(zhí)行定義的Java程序(字節(jié)碼)。前面在準(zhǔn)備階段變量被賦以所屬數(shù)據(jù)類型的默認(rèn)值,在初始化階段是通過程序制定的值去進(jìn)行變量的初始化。
初始化階段是執(zhí)行類的<clinit()>方法的過程:
?、?lt;clinit()>方法是有編譯器自動收集的所有類變量的賦值動作和static塊中的語句產(chǎn)生的(順序就是在源文件中定義的順序出現(xiàn)的,所以在靜態(tài)語句塊之中,只能定義在其后定義的static變量而不能訪問)

②<clinit()>方法和類的構(gòu)造器(這里指的是實(shí)例化的構(gòu)造器<init>())不同,它不需要顯示的調(diào)用分類的構(gòu)造器,因?yàn)樘摂M機(jī)會保證在子類的<clinit()>方法執(zhí)行之前一定會將父類的<clinit()>方法執(zhí)行完畢(側(cè)面說明虛擬機(jī)中第一個被執(zhí)行的<clinit()>肯定是java.lang.Object的)
?、塾傻冖跅l可以得出的是,由于父類的<clinit()>方法先執(zhí)行,所以在父類中定義的static語句塊要先于子類的變量賦值操作。下面的測試代碼中輸出的Son類的testSon值應(yīng)該是2,而不是1。
1package cn.jvm.classLoad; 2 3class parent { 4publicstaticinttestPar = 1; 5static { 6testPar = 2; 7? ? } 8} 910classSonextends parent {11publicstaticinttestSon = testPar;12}1314publicclass TestClass5 {15publicstaticvoid main(String[] args) {16? ? ? ? System.out.println(Son.testSon);17? ? }18}
④如果一個類中沒有靜態(tài)語句塊,也沒有變量賦值的操作,那么編譯器可以不用為這個類生成<client()>方法;
?、萁涌谥须m然沒有靜態(tài)語句塊,但是可以存在變量賦值的操作,所以接口中也會生成?<client()>方法。但是接口中的<client()>方法不需要先執(zhí)行父接口中的<client()>方法方法,只需要在父接口中變量被使用的時候才會初始化,同理接口的實(shí)現(xiàn)類在初始化的時候也不需要先執(zhí)行接口中的<client()>方法;
?、尢摂M機(jī)會保證一個類在多線程環(huán)境中的<client()>方法被正確加鎖、同步。如果多個線程去同時初始化一個類,那么只有一個線程會執(zhí)行<client()>方法,其他的線程會阻塞等待知道執(zhí)行完畢。
三、再看堆、棧、方法區(qū)
可以參考前面的JVM自動內(nèi)存管理機(jī)制-Java內(nèi)存區(qū)域(上)中講到的這三個區(qū)域的詳細(xì)概念和聯(lián)系,這里我們通過一個簡單的程序并結(jié)合類加載的過程來看一下這三者的關(guān)系。首先先簡單描述一下方法區(qū)和堆區(qū),方法區(qū):用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù);堆區(qū):存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存。
1package cn.jvm.classLoad; 2class TestDemo{ 3publicstaticinttest=100;//靜態(tài)變量,靜態(tài)域 4static{//靜態(tài)代碼塊 5System.out.println("靜態(tài)初始化類A"); 6test = 300 ; 7? ? } 8public TestDemo() { 9System.out.println("創(chuàng)建A類對象的實(shí)例化構(gòu)造方法");10? ? }11}12publicclass TestClass4 {13publicstaticvoid main(String[] args) {14TestDemo testDemo =new TestDemo();15? ? ? ? System.out.println(testDemo.test);16? ? }17}
我們先看下上面的代碼,代碼中TestDemo類中定義了靜態(tài)區(qū)域(包括代碼塊和靜態(tài)變量),然后在main中實(shí)例化TestDemo類的對象并訪問他的靜態(tài)變量,這里我們先再看一下這張圖,結(jié)合這個圖(簡單描述堆、棧、方法區(qū)的關(guān)系)通過類加載的過程來具體的分析一下堆、棧、方法區(qū)在類加載過程以及完畢之后里面存放的信息。

①JVM加載TestClass的時候,首先在方法區(qū)中形成TestClass類對應(yīng)靜態(tài)數(shù)據(jù)(類變量、類方法、代碼…),同時在堆里面也會形成java.lang.Class對象(反射對象),代表TestClass類,通過對象可以訪問到類二進(jìn)制結(jié)構(gòu)(方法區(qū))。然后加載TestDemo類信息,同時也會在堆里面形成TestDemo對象,代表TestDemo類。
②main方法執(zhí)行時會在棧里面形成main方法棧幀,一個方法對應(yīng)一個棧幀。如果main方法調(diào)用了別的方法,會在棧里面挨個往里壓,main方法里面有個局部變量A類型的a,一開始testDemo值為null,通過new調(diào)用類A的構(gòu)造器,棧里面生成TestDemo()方法同時堆里面生成testDemo對象,然后把TestDemo類的對象地址賦給棧中的testDemo,此時testDemo擁有類TestDemo的對象的地址。
③當(dāng)調(diào)用testDemo.test時,調(diào)用方法區(qū)數(shù)據(jù)。
反正總結(jié)下來就是:類加載最終在堆區(qū)中生成的Class 對象、堆中的Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。