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