Class文件的加載過程
ClassLoader的工作模式
類的熱加載
1 Class文件的裝載流程
只有被java虛擬機裝載的Class類型才能在程序中使用(注意裝載和加載的區別)
1.1 類裝載的條件
Class只有在必須要使用的時候才會被裝載,Java虛擬機不會無條件的裝載Class類型。Java虛擬機規定:一個類或者接口在初次使用時,必須進行初始化。這里的使用指的是主動使用,主動使用有以下幾種情況:
- 當創建一個類的實例時,比如使用new關鍵字,或者通過反射、克隆、反序列化。
- 當調用類的靜態方法時,即當使用了字節碼invokestatic指令
- 當使用類或者接口的靜態字段時(final常量除外),即使用getstatic或者putstatic指令
- 當使用java.lang.reflect包中的方法反射類的方法時
- 當初始化子類時,必須先初始化父類
- 作為啟動虛擬機、含有main方法的那個類
除了以上情況屬於主動使用外,其他情況均屬於被動使用,被動使用不會引起類的初始化。
例1:主動使用
public class Parent{
static{
System.out.println("Parent init");
}
}
public class Child{
static{
System.out.println("Child init");
}
}
public class InitMain{
public static void main(String[] args){
Child c = new Child();
}
}
以上聲明了3個類:Parent Child InitMain,Child類為Parent類的子類。若Parent被初始化,根據代碼中的static塊可知,將會打印"Parent init",若Child被初始化,則會打印"Child init"。執行InitMain,結果為:
Parent init
Child init
由此可知,系統首先裝載Parent類,接着裝載Child類。符合主動裝載中的兩個條件,使用new關鍵字創建類的實例會裝載相關的類,以及在初始化子類時,必須先初始化父類。
例2 :被動裝載
public class Parent{
static{
System.out.println("Parent init ");
}
public static int v = 100; //靜態字段
}
public class Child extends Parent{
static{
System.out.println("Child init");
}
}
public class UserParent{
public static void main(String[] args){
System.out.println(Child.v);
}
}
Parent中有靜態變量v,並且在UserParent中,使用其子類Child去調用父類中的變量。
運行代碼:
Parent init
100
雖然在UserParent中,直接訪問了子類對象,但是Child子類並未初始化,只有Parent父類進行初始化。所以,在引用一個字段時,只有直接定義該字段的類,才會被初始化。
注意:雖然Child類沒有被初始化,但是,此時Child類已經被系統加載,只是沒有進入初始化階段。
可以使用-XX:+ThraceClassLoading 參數運行這段代碼,查看日志,便可以看到Child類確實被加載了,只是初始化沒有進行
例3 :引用final常量
public class FinalFieldClass{
public static final String constString = "CONST";
static{
System.out.println("FinalFieldClass init");
}
}
public class UseFinalField{
public static void main(String[] args){
System.out.println(FinalFieldClass.constString);
}
}
運行代碼:CONST
FinalFieldClass類沒有因為其常量字段constString被引用而初始化,這是因為在Class文件生成時,final常量由於其不變性,做了適當的優化。
分析UseFinalField類生成的Class文件,可以看到main函數的字節碼為:
在字節碼偏移3的位置,通過Idc將常量池第22項入棧,在此Class文件中常量池第22項為:
#22 = String #23 //CONST
#23 = UTF8 CONST
由此可以看出,編譯后的UseFinalField.class中,並沒有引用FinalFieldClass類,而是將其final常量直接存放在常量池中,因此,FinalFiledClass類自然不會被加載。(javac在編譯時,將常量直接植入目標類,不再使用被引用類)通過捕獲類加載日志(部分日志)可以看出:
注意:並不是在代碼中出現的類,就一定會被加載或者初始化,如果不符合主動使用的條件,類就不會被初始化。
1.2 類裝載的整個過程
1)加載類
加載類處於類裝載的第一個階段。
加載類時,JVM必須完成:
- 通過類的全名,獲取類的二進制數據流
- 解析類的二進制數據流為方法區內的數據結構
- 創建java.lang.Class類的實例,表示該類型
2)連接
1 驗證類:
當類被加載到系統后,就開始連接操作,驗證是連接的第一步。
主要目的是保證加載的字節碼是符合規范的。驗證的步驟如圖:
2 准備
當一個類驗證通過后,虛擬機就會進入准備階段,在這個階段,虛擬機會為這個類分配相應的內存空間,並設置初始值。
java虛擬機為各種類型變量默認的初始值如表:
類型 | 默認初始值 |
int | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
boolean | false |
reference | null |
float | 0f |
double | 0f |
注意:java並不支持boolean類型,對於boolean類型,內部實現是Int,由於int的默認值是0,故對應的,boolean的默認值是false
如果類屬於常量字段,那么常量字段也會在准備階段被附上正確的值,這個賦值屬於java虛擬機的行為,屬於變量的初始化。事實上,在准備階段,不會有任何java代碼被執行。
3 解析類
在准備階段完成后,就進入了解析階段。
解析階段的任務就是將類、接口、字段和方法的符號引用轉為直接引用。
符號引用就是一些字面量的引用,和虛擬機的內部數據結構和內存布局無關。比較容易理解的就是在Class類文件中,通過常量池進行大量的符號引用。
具體可以使用JclassLib軟件查看Class文件的結構:::
3)初始化
初始化時類裝載的最后一個階段。如果前面的步驟沒有出現問題,那么表示類可以順利裝載到系統中。此時,類才會開始執行java字節碼。
初始化階段的重要工作是執行類的初始化方法<clinit>。方法<clinit>是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句塊合並產生的。
例如:
public class SimpleStatic{
public static int id = 1;
public static int number;
static{
number = 4;
}
}
java編譯器為這段代碼生成如下的<clinit>:
0 iconst_1
1 putstatic #2 <Demo.id>
4 iconst_4
5 putstatic #3 <Demo.number>
8 return
可以看出,生成的<clinit>函數中,整合了SimpleStatic類中的static賦值語句以及static語句塊,先后對id和number兩個成員變量進行賦值
由於在加載一個類之前,虛擬機總是會試圖加載該類的父類,因此父類的<clinit>總是在子類<clinit>之前被調用。也就是說,子類的static塊優先級高於父類。
public class ChildStatic extends Demo{
static{
number = 2;
}
public static void main(String[] args){
System.out.println(number);
}
}
運行可知:
2
說明父類的<clinit>總是在子類<clinit>之前被調用。
注意:java編譯器並不是為所有的類都產生<clinit>初始化函數,如果一個類既沒有賦值語句,也沒有static語句塊,那么生成的<clinit>函數就應該為空,因此,編譯器就不會為該類插入<clinit>函數
例如:
public class StaticFinalClass{
public static final int i=1;
public static final int j=2;
}
由於StaticFinalClass只有final常量,而final常量在准備階段初始化,而不在初始化階段處理,因此對於StaticFinalClass類來說,<clinit>就無事可做,因此,在產生的class文件中沒有該函數存在。