小米二面:JVM 觸發(fā)類加載的條件有哪些?我說 new 的時(shí)候加載,然后他對我笑了笑......
Java 虛擬機(jī)(JVM)中,類的加載并不是隨意發(fā)生的,而是由特定的觸發(fā)條件決定的。什么時(shí)候加載?什么時(shí)候初始化?
這是我們必須要搞清楚的問題,尤其在復(fù)雜的應(yīng)用中,弄懂類加載的時(shí)機(jī)能幫助我們避免一些潛在的性能問題和運(yùn)行時(shí)錯(cuò)誤。
在本節(jié)中,我們將詳細(xì)探討類加載的時(shí)機(jī)、主動(dòng)和被動(dòng)引用的區(qū)別,以及常見的類加載觸發(fā)條件。
類加載生命周期
類加載的生命周期包括:加載(Loading)、鏈接(Linking) 和 初始化(Initialization)。而其中,初始化階段是決定類是否被真正加載的關(guān)鍵。
JVM 在什么時(shí)候啟動(dòng)類加載過程呢?
主要分為主動(dòng)引用和被動(dòng)引用兩種情況。我們分別看看這兩種情況在什么條件下會(huì)觸發(fā)類加載。
主動(dòng)引用
主動(dòng)引用是指程序顯式地使用某個(gè)類,從而觸發(fā)類的加載和初始化。根據(jù)《Java 虛擬機(jī)規(guī)范》,以下六種情況會(huì)觸發(fā)類的主動(dòng)引用,也就是觸發(fā)類加載的條件!
1. 創(chuàng)建類的實(shí)例
當(dāng)你使用 new 關(guān)鍵字創(chuàng)建一個(gè)類的實(shí)例時(shí),JVM 會(huì)立即加載并初始化該類。
// 觸發(fā) MyClass 的加載和初始化
MyClass obj = new MyClass();
初始化流程:
- 分配內(nèi)存給 MyClass 的實(shí)例對象。
- 加載 MyClass 類的字節(jié)碼,并執(zhí)行靜態(tài)代碼塊和靜態(tài)變量賦值操作。
2. 訪問類的靜態(tài)字段或靜態(tài)方法
訪問類的靜態(tài)字段或靜態(tài)方法時(shí),也會(huì)觸發(fā)類的加載和初始化。
// 觸發(fā) MyClass 的加載
System.out.println(MyClass.staticVar);
// 觸發(fā) MyClass 的加載
MyClass.staticMethod();
常量不會(huì)觸發(fā)類加載:如果靜態(tài)字段是 final 修飾的常量,它在編譯期已存入常量池,因此不會(huì)觸發(fā)類加載。
System.out.println(MyClass.FINAL_CONSTANT); // 不觸發(fā)類加載
3. 反射
通過反射調(diào)用類時(shí),也會(huì)觸發(fā)類加載。
Class<?> clazz = Class.forName("com.example.MyClass"); // 觸發(fā) MyClass 的加載
4. 初始化類的子類時(shí),先初始化父類
當(dāng)初始化一個(gè)類時(shí),如果它的父類尚未初始化,JVM 會(huì)先初始化父類。
public class Parent {
static {
System.out.println("父類初始化");
}
}
public class Child extends Parent {
static {
System.out.println("子類初始化");
}
}
// 先輸出"父類初始化",再輸出"子類初始化"
Child obj = new Child();
5. 擬機(jī)啟動(dòng)時(shí),初始化 main 方法所在的類
虛擬機(jī)啟動(dòng)時(shí),main 方法所在的類是程序的入口類,會(huì)被優(yōu)先加載和初始化。
public static void main(String[] args) {
System.out.println("主類加載");
}
6. 動(dòng)態(tài)語言支持
在 Java 7 引入的 java.lang.invoke 包中,當(dāng) MethodHandle 最終指向的類需要初始化時(shí),也會(huì)觸發(fā)類的加載。
MethodHandle handle = MethodHandles.lookup().findStatic(MyClass.class, "staticMethod", MethodType.methodType(void.class));
handle.invoke(); // 可能觸發(fā) MyClass 的加載
被動(dòng)引用:不觸發(fā)類加載
與主動(dòng)引用相對,被動(dòng)引用是指訪問類的某些特性時(shí)不會(huì)觸發(fā)類的加載和初始化。以下是幾種典型的被動(dòng)引用場景。
1. 通過子類引用父類的靜態(tài)字段
如果子類只引用父類的靜態(tài)字段,JVM 只會(huì)初始化父類,而不會(huì)初始化子類。
示例
// 只觸發(fā) Parent 的加載,不觸發(fā) Child 的加載
System.out.println(Child.staticVar);
2. 訪問編譯期常量
訪問 final 修飾的編譯期常量,不會(huì)觸發(fā)類的加載。
// 不觸發(fā) MyClass 的加載
System.out.println(MyClass.FINAL_CONSTANT);
3. 通過數(shù)組定義類引用
通過數(shù)組引用一個(gè)類,不會(huì)觸發(fā)該類的加載。
// 不觸發(fā) MyClass 的加載
MyClass[] array = new MyClass[10];
碼哥,為什么需要關(guān)注類加載的時(shí)機(jī)?
- 避免類的過早加載:過早加載可能導(dǎo)致不必要的內(nèi)存消耗,尤其在大型應(yīng)用中。
- 延遲加載(Lazy Loading):通過延遲加載,可以在真正需要時(shí)才加載類,減少啟動(dòng)時(shí)間。
- 減少類加載沖突:在模塊化或插件化的應(yīng)用中,合理安排類加載順序有助于避免類沖突和類加載死鎖問題。