類加載過程
Class文件被加載到內存,並對數據進行校驗、轉換解析和初始化,最終卸載出內存,這個過程被稱作類加載過程
。類加載過程包括加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個階段統稱爲連接。
加載
加載
是類加載
過程的第一個階段。在加載階段,虛擬機要完成以下3件事情:
- 通過一個類的全限定名來獲取該類的二進制字節流。
- 將靜態的字節流轉化爲方法區運行時數據結構。
- 在Java堆中生成一個java.lang.Class對象,作爲訪問方法區各種數據的入口。
驗證
驗證
是指對Class文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
文件格式驗證
是基於字節流的驗證。驗證字節流符合當前的Class文件格式的規範,能被當前虛擬機處理。驗證通過後,字節流纔會進入內存的方法區進行存儲。元數據驗證
是基於方法區的存儲結構驗證。對字節碼進行語義驗證,確保不存在不符合Java語言規範的元數據信息。字節碼驗證
是基於方法區的存儲結構驗證,通過對數據流和控制流的分析,保證被檢驗類的方法在運行時不會做出危害虛擬機的動作。符號引用驗證
是基於方法區的存儲結構驗證,發生在解析階段,確保能夠將符號引用成功的解析爲直接引用,其目的是確保解析動作正常執行。換句話說就是對類自身以外的信息進行匹配性校驗。
準備
準備
是正式爲類變量(靜態變量,被static修飾的變量)分配內存並設置類變量的初始值。這些變量所使用的內存都將在方法區中進行分配(JDK 8及之後,類變量則會隨着Class對象一起存放在Java堆中)。這個階段要注意三點:
- 這個時候進行內存分配的僅包括類變量(靜態變量,被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。
- 這裏所說的初始值"通常情況"下是數據類型的零值,假設一個類變量的定義爲:
public static int value = 123;
變量value在準備階段會賦值爲0而不是123。在初始化階段調用類構造器時,纔會賦值爲123。
- 初始值"通常情況"下是數據類型的零值,也就是說還有特殊情況。被static final修飾的常量對value直接賦值爲123。
解析
解析
是將符號引用轉變爲直接引用,直接引用實際上是指向了真實的內存地址。解析包括:類或接口的解析,字段解析,類方法解析,接口方法解析。
比如在D類中引用了C類。在編譯階段,D類並不知道C類的實際內存地址,因此只能用C類的符號引用來代替。在解析階段,JVM可以通過解析C類的符號引用,來確定C類的真實內存地址(如果該類未被加載過,則先加載)。
初始化
初始化
實際上是調用類構造器<clinit>()。在編譯階段,所有的類變量、靜態代碼塊都會收斂到類構造器中,類初始化階段所有的靜態變量會被重新賦值, 並且執行靜態塊。
類初始化時機
,Java虛擬機規範嚴格的規定了有且只有6種情況必須立即對類進行"初始化"。
- 遇到new、getstatic、setstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態子段(被final修飾、已在編譯期把結果放入常量池的靜態子段除外)的時候,以及調用一個類的靜態方法的時機。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
- 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先觸發其初始化。
- 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。