定義
虛擬機把描述類的數據從Class文件中加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
生命周期
類從被加載到虛擬機內存開始,到卸載出內存為止,整個生命周期包括:加載、鏈接、初始化、使用、卸載,其中鏈接又包括驗證、準備、解析。
類的初始化
虛擬機規范嚴格規定了有且僅有5種情況必須立即對類進行初始化:
- 遇到new,getstatic,putstatic或invokestatic這4條字節碼指令時,如果類還沒有進行初始化,則需要先觸發其初始化。例如:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態屬性的時候、以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類還沒有進行初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行初始化時,則需要先觸發父類的初始化。
- 當虛擬機啟動的時候,用戶需要指定一個要執行的主類,虛擬機會先初始化這個類。
- 如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種場景中的行為稱為對一個類進行主動引用,除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用,例如:
- 通過子類引用父類的靜態字段,不會導致子類初始化
public class Father {
static {
System.out.println("load father");
}
public static int value = 1;
}
class Child extends Father{
static {
System.out.println("load child");
}
}
public static void main(String[] args) {
System.out.println(Child.value);
}
上述代碼不會輸出"load child";因此通過子類來引用父類中定義的靜態字段,不會觸發子類的初始化。
- 通過數組定義來引用類,不會觸發此類的初始化
public static void main(String[] args) {
Father[] fathers = new Father[2];
}
上述代碼不會輸出"load father";說明沒有觸發Father類的初始化。
- 引用常量不會觸發此類的初始化
public class Constant {
static {
System.out.println("load Constant");
}
public static final String HELLO = "hello";
}
public class TestDemo {
public static void main(String[] args) {
System.out.println(Constant.HELLO);
}
}
上述代碼不會輸出"load Constant";這是因為雖然在Constant中定義了常量HELLO,但其實在編譯階段通過常量傳播優化,已經將此常量的值"hello"存儲到了TestDemo類的常量池中,以后TestDemo對常量Constant.HELLO的引用實際被轉化為TestDemo類對自身常量池的引用了。也就是說,實際上TestDemo的Class文件之中并沒有Constant類的符號引用入口,這兩個類在編譯成Class之后就不存在任何聯系了。
接口的初始化
接口也有初始化過程,上面類的初始化是通過靜態代碼塊"static{}"來輸出初始化信息的,而接口中不能使用"static{}",但編譯器任然會為接口生成"<clinit>()"類構造器,用于初始化接口中所定義的成員變量。接口與類真正的區別是前面5種初始化場景中的第3種:當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,并不要求其父類全部都完成了初始化,只有在真正使用到父類接口的時候才會初始化。