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