一、類加載機制
一個.java文件在編譯后會形成相應的一個或多個Class文件,這些Class文件中描述了類的各種信息,並且它們最終都需要被加載到虛擬機中才能被運行和使用。
JVM把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被JVM直接使用的Java類型的過程就是類加載機制。
二、類加載過程
Java類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、准備、解析、初始化、使用、卸載七個階段。
其中准備、驗證、解析3個部分統稱為連接。類加載過程包括:加載、連接、初始化。
類的加載過程必須按照這種順序按部就班地開始,而不是按部就班的“進行”或“完成”,因為這些階段通常都是相互交叉地混合式進行的,也就是說通常會在一個階段執行的過程中調用或激活另外一個階段。
1、加載
在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下三件事情:
(1)通過一個類的全限定名來獲取定義此類的二進制字節流;
(2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
(3) 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口;
加載階段和連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這兩個階段的開始時間仍然保持着固定的先后順序。
2、驗證
目的:確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
驗證階段大致會完成4個階段的檢驗動作:
文件格式驗證:驗證字節流是否符合Class文件格式的規范(例如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型)
元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規范的要求(例如:這個類是否有父類,除了java.lang.Object之外);
字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的;
符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響。如果所引用的類經過反復驗證,那么可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3、准備
准備階段是正式為類變量(static 成員變量)分配內存並設置類變量初始值(零值)的階段,這些變量所使用的內存都將在方法區中進行分配。
public static int value = 123; // 經過准備階段 value==0; 把value賦值為123的動作將在初始化階段才會執行。
public static final int value = 123; // 經過准備階段 value==123;
4、解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
5、初始化
初始化階段是執行類構造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊static{}中的語句合並產生的。如果一個類中沒有靜態語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生產類構造器<clinit>()。
順序:
(1)編譯器收集的順序是由語句在源文件中出現的順序所決定的,定義在靜態語句塊之前的變量 靜態語句塊中可以隨意訪問,定義在靜態語句塊之后的變量 靜態語句塊不能訪問 但是可以賦值(因為准備階段分配過內存空間了)。
(2)先執行父類的類構造<clinit>(),后執行子類類構造器<clinit>()。意味着父類中定義的靜態語句塊/靜態變量的初始化要優先於子類的靜態語句塊/靜態變量的初始化執行。
舉例按順序執行:
public class Test{ static{ i=0; System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前應用) } static int i=1; }
並發:
虛擬機會保證一個類的類構造器<clinit>()在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的類構造器<clinit>(),其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。
特別需要注意的是,在這種情形下,其他線程雖然會被阻塞,但如果執行<clinit>()方法的那條線程退出后,其他線程在喚醒之后不會再次進入/執行<clinit>()方法,因為在同一個類加載器下,一個類型只會被初始化一次。
初始化時機:
虛擬機規范指明有且只有五種情況必須立即對類進行初始化(而這一過程自然發生在加載、驗證、准備之后):
(1) 遇到new、getstatic、putstatic或invokestatic這四條字節碼指令,生成這四條指令的最常見的Java代碼場景是:
使用new關鍵字實例化對象的時候;
讀取或設置一個類的靜態字段的時候;(static final修飾的變量在編譯器把結果放入常量池,不需要初始化)
調用一個類的靜態方法的時候。
(2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
(4)當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
(5)當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
這五種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為被動引用。被動引用的幾種經典場景:
(1)通過子類引用父類的靜態字段,不會導致子類初始化。
(2)通過數組定義來引用類,只是數組類型本身的初始化,不會觸發此類的初始化。
比如,new String[]只會直接觸發String[]類的初始化,也就是觸發對類[Ljava.lang.String的初始化,而直接不會觸發String類的初始化。
(3)常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。(准備階段已經賦值,所以不需要初始化)
舉例1:子類引用父類的靜態字段,不會導致子類初始化。
public class SSClass{ static{ System.out.println("SSClass"); } } public class SClass extends SSClass{ static{ System.out.println("SClass init!"); } public static int value = 123; public SClass(){ System.out.println("init SClass"); } } public class SubClass extends SClass{ static{ System.out.println("SubClass init"); } static int a; public SubClass(){ System.out.println("init SubClass"); } } public class NotInitialization{ public static void main(String[] args){ System.out.println(SubClass.value); } } /* Output: SSClass SClass init! 123 */
舉例2:調用常量,不會觸發定義常量的類的初始化。
public class ConstClass{ static{ System.out.println("ConstClass init!"); } public static final String CONSTANT = "hello world"; } public class NotInitialization{ public static void main(String[] args){ System.out.println(ConstClass.CONSTANT); } } /* Output: hello world */