本文根據(jù)《深入理解java虛擬機(jī)》第7章內(nèi)容整理
一、基本概念
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
與那些在編譯時(shí)需要進(jìn)行鏈接工作的語(yǔ)言不同,在Java語(yǔ)言里,類型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的,這種策略雖然會(huì)令類加載時(shí)稍微增加一些性能開(kāi)銷,但是會(huì)為Java應(yīng)用程序提供高度的靈活性,Java可以動(dòng)態(tài)擴(kuò)展的語(yǔ)言特性就是依賴運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)鏈接這個(gè)特點(diǎn)實(shí)現(xiàn)的。
類的生命周期:
類從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接。
這些階段通常都是互相交叉地混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用、激活另一個(gè)階段。
例如:加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼的文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開(kāi)始,但這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的內(nèi)容,這兩個(gè)階段的開(kāi)始時(shí)間仍然保持著固定的先后順序。
二、類加載的時(shí)機(jī)
Java虛擬機(jī)規(guī)范沒(méi)有強(qiáng)制性約束在什么時(shí)候開(kāi)始類加載過(guò)程,但是對(duì)于初始化階段,虛擬機(jī)規(guī)范則嚴(yán)格規(guī)定了有且只有5種情況必需立即對(duì)類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備階段自然需要在此之前開(kāi)始)。
遇到
new
、getstatic
、putstatic
或invokestatic
這4條字節(jié)碼指令時(shí),如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
生成這4條指令最常見(jiàn)的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象時(shí)、讀取或者設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)時(shí)、以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。使用
java.lang.reflect
包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類的初始化。
當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含
main()
方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)類。當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)
java.lang.invoke.MethodHandle
實(shí)例最后的解析結(jié)果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
對(duì)于這5種會(huì)觸發(fā)類進(jìn)行初始化的場(chǎng)景,在java虛擬機(jī)規(guī)范中限定了“有且只有”這5種場(chǎng)景會(huì)觸發(fā)。
這5種場(chǎng)景中的行為稱為對(duì)一個(gè)類的主動(dòng)引用,除此以外的所有引用類的方式都不會(huì)觸發(fā)類的初始化,稱為被動(dòng)引用。
被動(dòng)引用示例:
- 通過(guò)子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化。
public class SuperClass {
static{
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static{
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
SuperClass init!
123
對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過(guò)其子類來(lái)引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類的初始化。
- 通過(guò)數(shù)組定義來(lái)引用類,不會(huì)觸發(fā)此類的初始化。
public class SuperClass {
static{
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] scs = new SuperClass[10];
}
}
輸出結(jié)果為空
沒(méi)有輸出SuperClass init!
說(shuō)明沒(méi)有觸發(fā)類com.zm.classloading.SuperClass
的初始化階段,但是這段代碼會(huì)觸發(fā)[Lcom.zm.classloading.SuperClass
類的初始化階段。這個(gè)類是由虛擬機(jī)自動(dòng)生成的,直接繼承于java.lang.Object
的子類,創(chuàng)建動(dòng)作由字節(jié)碼指令 newarray
觸發(fā)。
- 常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。
public class ConstClass {
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
hello world
雖然在Java源碼中引用了ConstClass類中的常量HELLOWORLD
,但其實(shí)在編譯階段通過(guò)常量傳播優(yōu)化,已經(jīng)將此常量的值hello world
存儲(chǔ)到了NotInitialization類的常量池中,以后NotInitialization對(duì)于常量ConstClass.HELLOWORLD
的引用實(shí)際上都被轉(zhuǎn)化為NotInitialization類對(duì)自身常量池的引用了。實(shí)際上NotInitialization的Class文件之中已經(jīng)不存在ConstClass類的符號(hào)引用入口了。
接口的加載過(guò)程:
接口也有初始化過(guò)程,這與類是一致的,上述的代碼中都是使用靜態(tài)語(yǔ)句塊static{}
來(lái)輸出初始化信息的,而接口中不能使用static{}
語(yǔ)句塊,但編譯器仍然會(huì)為接口生成<clinit>()
類構(gòu)造器,用于初始化接口中所定義的成員變量。
接口的加載過(guò)程與類加載的區(qū)別在于上面提到的5種“有且僅有”需要初始化場(chǎng)景中的第3種:當(dāng)一個(gè)類在初始化時(shí)要求其父類全部都已經(jīng)初始化過(guò)了,但是一個(gè)接口在初始化時(shí),并不要求其父接口都全部完成了初始化,只有在真正用到父接口的時(shí)候(如引用父接口中定義的常量)才會(huì)初始化。
三、類加載的過(guò)程
下面我們來(lái)詳細(xì)了解類加載的全過(guò)程,也就是加載、驗(yàn)證、準(zhǔn)備、解析和初始化這五個(gè)階段的過(guò)程。
-
加載
首先要說(shuō)明的是“加載”(Loading)階段只是“類加載”(Class Loading)過(guò)程的一個(gè)階段。不要混淆了這兩個(gè)概念。在加載階段,虛擬機(jī)需要完成以下三件事情:1)通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
3)在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class
對(duì)象,作為方法區(qū)這個(gè)類各種數(shù)據(jù)的訪問(wèn)入口。
相對(duì)于類加載過(guò)程的其他階段,一個(gè)非數(shù)組類的加載階段(準(zhǔn)確的說(shuō),是加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的,因?yàn)樵撾A段既可以使用系統(tǒng)提供的引導(dǎo)類加載器完成,也可以由用戶自定義的類加載器來(lái)完成,開(kāi)發(fā)人員可以通過(guò)定義自己的類加載器去控制字節(jié)流的獲取方式。
對(duì)于數(shù)組類而言,數(shù)組類本身不通過(guò)類加載器創(chuàng)建,它是由虛擬機(jī)直接創(chuàng)建的。但是數(shù)組類的元數(shù)據(jù)類型最終還是要靠類加載器去創(chuàng)建的。
加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由JVM自行定義。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class
對(duì)象,這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中這些類型數(shù)據(jù)的外部接口。(對(duì)于HotSpot虛擬機(jī)而言,Class對(duì)象比較特殊,它雖然是對(duì)象,但是存放在方法區(qū)里面)
-
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
整體上看,驗(yàn)證階段會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
- 1)文件格式驗(yàn)證:這一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。
- 是否以魔術(shù)0xCAFEBABE開(kāi)頭;
- 主次版本號(hào)是否是在當(dāng)前虛擬機(jī)處理范圍之內(nèi);
- 常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志);
. . .
第一階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合一個(gè)Java類型信息的要求。
這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面的3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
- 2)元數(shù)據(jù)驗(yàn)證:這一階段主要是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求。
- 這個(gè)類是否有父類;
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類);
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法;
- 類中的字段、方法是否與父類產(chǎn)生矛盾(如覆蓋了父類的final字段,不符合規(guī)則的重載);
. . .
第二階段的主要目的是對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在不符合Java規(guī)范的元數(shù)據(jù)類型。
3)字節(jié)碼驗(yàn)證:這一階段是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段,主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析。保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體之外的字節(jié)碼指令上;
保證方法體中的類型轉(zhuǎn)換是有效的,例如將子類對(duì)象賦給父類對(duì)象是安全的,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型,甚至是和它毫無(wú)繼承關(guān)系的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不安全的;
保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)在操作數(shù)棧中放置了一個(gè)int類型數(shù)據(jù),使用時(shí)卻按long類型來(lái)加載人本地變量表中。
. . .4)符號(hào)引用驗(yàn)證:這一階段主要是在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候進(jìn)行校驗(yàn),這個(gè)轉(zhuǎn)化動(dòng)作是發(fā)生在解析階段。符號(hào)引用可以看做是對(duì)類自身以外(常量池的各種符號(hào)引用)的信息進(jìn)行匹配性的校驗(yàn)。
符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到相應(yīng)的類;
在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述方法和字段;
符號(hào)引用中的類、字段、方法的訪問(wèn)性(
private、public、protected、default
)是否可以被當(dāng)前類訪問(wèn);
. . .
符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,那么將會(huì)拋出異常。
驗(yàn)證階段對(duì)于虛擬機(jī)的類加載機(jī)制來(lái)說(shuō),是一個(gè)非常重要但不一定是必要的階段。如果所運(yùn)行的全部代碼都已經(jīng)被反復(fù)使用和驗(yàn)證過(guò),在實(shí)施階段就可以考慮使用-Xverify:none
參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,從而縮短虛擬機(jī)類加載的時(shí)間。
-
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。
- 這個(gè)時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起被分配在Java堆中。
- 這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類型的零值,例如
public static int value = 123
;value在準(zhǔn)備階段后的初始值是0而不是123,因?yàn)榇藭r(shí)尚未執(zhí)行任何的Java方法,而把value賦值為123的putStatic指令是程序被編譯后,存放在類構(gòu)造器<clinit>()方法之中,把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。 - 通常情況下初始值為零值,相對(duì)的會(huì)存在特殊情況:如果類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量就會(huì)被初始化為ConstantValue屬性所指定的值,例如
public static final int value = 123
編譯時(shí)javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將變量賦值為123。
-
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。
在Class文件中符號(hào)引用以CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等類型的常量出現(xiàn)。
-
符號(hào)引用(Symbolic Reference):
符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)引用可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義的定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定已經(jīng)加載在內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是他們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中。 -
直接引用(Direct Reference):
直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般都不相同,如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
對(duì)于同一個(gè)符號(hào)引用可能會(huì)出現(xiàn)多次解析請(qǐng)求,虛擬機(jī)可能會(huì)對(duì)第一次解析的結(jié)果進(jìn)行緩存。
解析動(dòng)作主要針對(duì):類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。
個(gè)人理解:一個(gè)java類將會(huì)編譯成一個(gè)class文件。在編譯時(shí),java類并不知道引用類的實(shí)際內(nèi)存地址,因此只能使用符號(hào)引用來(lái)代替。比如org.simple.People類引用org.simple.Tool類,在編譯時(shí)People類并不知道Tool類的實(shí)際內(nèi)存地址,因此只能使用符號(hào)org.simple.Tool(假設(shè))來(lái)表示Tool類的地址。而在類加載器加載People類時(shí),此時(shí)可以通過(guò)虛擬機(jī)獲取Tool類的實(shí)際內(nèi)存地址,因此便可以既將符號(hào)org.simple.Tool替換為Tool類的實(shí)際內(nèi)存地址,及直接引用地址。
-
初始化
類初始化階段是類加載過(guò)程的最后一步,前面的類加載過(guò)程中,除了加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開(kāi)始執(zhí)行類中定義的Java程序代碼。初始化階段是執(zhí)行類構(gòu)造器
<clinit>()
方法的過(guò)程。對(duì)于<clinit>()
方法具體介紹如下:
-
(1)
<clinit>()
方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}
塊)中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序由語(yǔ)句在源文件中出現(xiàn)的順序所決定。
靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問(wèn)。
public class Test {
static{
i =0; //給變量賦值可以正常編譯通過(guò)
// System.out.println(i); //這句編譯器會(huì)提示“非法向前引用”
}
static int i = 1;
}
(2)
<clinit>()
方法與類的構(gòu)造函數(shù)不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的<clinit>()
方法執(zhí)行之前,父類的<clinit>()
方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個(gè)執(zhí)行的<clinit>()
方法的類一定是java.lang.Object
。(3) 由于父類的
<clinit>()
方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作。如下面的例子所示,輸出結(jié)果為2而不是1。
public class Parent {
public static int A = 1;
static{
A = 2;
}
}
public class Sub extends Parent{
public static int B = A;
}
public class Test {
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
(4)
<clinit>()
方法對(duì)于類或者接口來(lái)說(shuō)并不是必需的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()
方法。(5) 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有變量賦值的初始化操作,因此接口也會(huì)生成
<clinit>()
方法。但是接口與類不同,執(zhí)行接口的<clinit>()
方法不需要先執(zhí)行父接口的<clinit>()
方法。只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也不會(huì)執(zhí)行接口的<clinit>()
方法。(6) 虛擬機(jī)會(huì)保證一個(gè)類的
<clinit>()
方法在多線程環(huán)境中被正確地加鎖和同步。如果有多個(gè)線程去同時(shí)初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()
方法,其它線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<clinit>()
方法完畢。如果在一個(gè)類的<clinit>()
方法中有耗時(shí)很長(zhǎng)的操作,那么就可能造成多個(gè)進(jìn)程阻塞。
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
[2015.08.31]