什么是類加載機(jī)制
JVM把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被JVM直接使用的Java類型,這就是JVM的類加載機(jī)制。
類的生命周期
類從被加載到內(nèi)存中,到被卸載出內(nèi)存,一共分為以下幾步:
- 加載(Loading)
- 驗(yàn)證(Verification)
- 準(zhǔn)備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
類加載的全過程,包括其中的加載、驗(yàn)證、準(zhǔn)備、解析、初始化
幾個(gè)階段。
加載
加載是類加載的第一階段,在這一步中JVM規(guī)范要求完成了以下三件事:
通過一個(gè)類的全限定名來獲取定義這個(gè)類的二進(jìn)制字節(jié)流。
將這個(gè)字節(jié)流多代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象。
以上要求其實(shí)并不具體,JVM的具體實(shí)現(xiàn)和應(yīng)用都是比較靈活的。比如:獲取這個(gè)類的二進(jìn)制字節(jié)流,并沒有說從哪獲取,怎么獲取,于是就有了從壓縮包中讀取(jar、war、ear)、從網(wǎng)絡(luò)中獲取(Applet)、運(yùn)行時(shí)計(jì)算生成(動(dòng)態(tài)代理)。對(duì)于不是數(shù)組的類的加載,我們可以定義自己的類加載器去控制字節(jié)流的獲取方式。但是,對(duì)于數(shù)組類就不一樣了,因?yàn)閿?shù)組類本身不是通過類加載器創(chuàng)建的,而是JVM直接創(chuàng)建的。
驗(yàn)證
這一階段是為了保證Class文件的字節(jié)流中包含的信息符合當(dāng)前JVM的要求,并且不危害JVM自身的安全。大致分為以下四個(gè)階段:
文件格式驗(yàn)證
驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,能不能被當(dāng)前JVM處理。驗(yàn)證點(diǎn)比較多,比如:是否以魔數(shù)0xCAFEBABE開頭、主次版本號(hào)是否在當(dāng)前JVM的處理范圍內(nèi)、常量池的常量是否有不被支持的常量類型、CONSTANT_Utf8_info類型的常量中是否有不符合UTF8編碼的數(shù)據(jù)等等。這個(gè)階段是基于二進(jìn)制字節(jié)流進(jìn)行驗(yàn)證的,只有這個(gè)階段驗(yàn)證通過了,字節(jié)流才能進(jìn)入內(nèi)存的方法區(qū)儲(chǔ)存。
元數(shù)據(jù)驗(yàn)證
這個(gè)階段主要是對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義分析和校驗(yàn),保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息。比如:除了java.lang.Object以外的類是否有父類、是否繼承了一個(gè)不允許被繼承的類、非抽象類是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法、是否覆蓋了父類的final字段等等。
字節(jié)碼校驗(yàn)
這個(gè)階段通過數(shù)據(jù)流和控制流分析,確保程序語(yǔ)義是合法的、符合邏輯的。比如:放置和使用操作棧時(shí)數(shù)據(jù)類型保證一致、保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上、保證方法體中的類型轉(zhuǎn)換是有效的等等。
符號(hào)引用校驗(yàn)
這個(gè)階段是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),它發(fā)生在解析步驟中,確保解析能正常執(zhí)行,比如:符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類、符號(hào)引用中的類字段方法的訪問性是否可以訪問當(dāng)前類等等。
準(zhǔn)備
在這個(gè)階段里,為靜態(tài)變量分配內(nèi)存并設(shè)置靜態(tài)變量初始值。這里說的初始值通常情況下,不是代碼中寫的初始值,而是數(shù)據(jù)類型的零值。代碼中寫的初始值,是在初始化階段賦值的。如果是靜態(tài)常量(被final修飾),這個(gè)階段就會(huì)被直接賦值為代碼中寫的初始值。
解析
在這個(gè)階段里,JVM把常量池內(nèi)的符號(hào)引用替換為直接引用。符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可,它和JVM實(shí)現(xiàn)的內(nèi)存布局無關(guān)。直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄,它是和JVM實(shí)現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用,那么引用的目標(biāo)肯定在內(nèi)存中存在。
解析主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符的符號(hào)引用進(jìn)行,分別對(duì)應(yīng)常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info。
初始化
初始化階段才真正開始執(zhí)行類中定義的字節(jié)碼,也是執(zhí)行類構(gòu)造器()方法的過程。()方法是由編譯器自動(dòng)收集類中的所有靜態(tài)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是用語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊只能訪問到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問。
JVM會(huì)保證在子類的()方法執(zhí)行之前,父類的()方法已經(jīng)執(zhí)行完畢,也就是說父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作。如果類沒有靜態(tài)語(yǔ)句塊,也沒有對(duì)靜態(tài)變量賦值,編譯器就不會(huì)為這個(gè)類生成()方法。接口的()方法不需要先執(zhí)行父接口的()方法,只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)被初始化。
JVM會(huì)保證一個(gè)類的()方法在多線程環(huán)境中被正確地加鎖、同步。如果一個(gè)線程在執(zhí)行這個(gè)類的()方法,其他線程都需要阻塞等待,當(dāng)()方法執(zhí)行完后,其他線程也不會(huì)再次進(jìn)入()方法。同一個(gè)類加載器下,一個(gè)類只會(huì)被初始化一次。
結(jié)語(yǔ)
這次我們了解了類加載過程的幾個(gè)階段,分別是加載、驗(yàn)證、準(zhǔn)備、解析和初始化。加載是把二進(jìn)制字節(jié)碼載入內(nèi)存,驗(yàn)證是校驗(yàn)字節(jié)流中包含的信息是否符合當(dāng)要求,準(zhǔn)備是為靜態(tài)變量分配內(nèi)存并設(shè)置靜態(tài)變量初始值,解析是把常量池內(nèi)的符號(hào)引用替換為直接引用,初始化是執(zhí)行所有靜態(tài)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句。