類加載機制
JVM虛擬機通過類加載器將描述類的.class文件中的數據讀入內存,並對數據進行驗證、解析和初始化,最終形成可以被虛擬機直接使用的Class對象。
類的加載過程
整個類的生命週期如下:
加載
將.class文件中的數據加載到內存並將其放入方法區內,然後在內存中創建一個java.lang.class對象(JVM規範並未規定class對象的位置該放在哪,然而Hotspot將其放在了方法區內)用來封裝類在方法區內的數據結構
驗證
- 文件格式驗證:驗證.class文件字節流是否符合class文件的格式的規範,並且能夠被當前版本的虛擬機處理。這裏面主要對魔數、主版本號、常量池等等的校驗
- 元數據驗證:主要是對字節碼描述的信息進行語義分析,以保證其描述的信息符合java語言規範的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類衝突等等。
- 字節碼驗證:主要是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在元數據驗證階段對數據類型做出 驗證後,這個階段主要對類的方法做出分析,保證類的方法在運行時不會做出威脅虛擬機安全的事
- 符號引用驗證:要是對類自身以外的信息進行校驗。目的是確保解析動作能夠完成。
準備
爲類的靜態變量分配內存並賦予一個默認初始值
解析
將類、接口、字段和方法的引用轉換爲直接引用
初始化
爲類的靜態變量分配正確的初始值(這個正確的初始值就是我們自定的那個值)
加載.class的方式
- 從本地系統中加載
- 通過網絡下載.class文件
- 從zip、jar的歸檔文件中加載.class文件
- 從專用的數據中提取.class文件(這種方式並不常見)
- 將java源文件動態編譯成.class文件(比如jsp最終會被編譯爲servlet)
類的使用方式
java程序對類的使用方式分爲兩種:主動使用、被動使用。
主動使用:
- 創建類的實例
- 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
- 調用類的靜態方法
- 反射(如Class.forName("com.test.Test"))
- 初始化一個類的子類
- 啓動類的類(Main方法所在的類)
除了上訴幾種,其他的基本上都是被動使用都不會導致初始化。但需要注意的是:引用該類靜態常量並不會導致該類的初始化,其原因是因爲程序編譯期間會將靜態常量放入調用類的方法區常量池中
public class MyTest1 {
public static void main(String[] args) {
System.out.println(Parent.a);
}
}
class Parent{
public static final int a = 1;
static {
System.out.println("Parent static");
}
}
通過運行代碼可以確定Parent類確實沒有被初始化,如果被初始化那它的靜態代碼塊一定會被執行。其實我們還可以通過javap命令解析一下這段代碼
iconst_1是JVM中的助記符,它表示將一個int型的數字1推送至棧頂,通過觀察可以確定並有對Parent類的任何引用,所有並不會導致Parent類的初始化。
對於調用程序編譯期間未知值的靜態常量(如new Random().nextInt()),會導致該類的初始化。
public class MyTest1 {
public static void main(String[] args) {
System.out.println(Parent.a);
}
}
class Parent{
public static final int a = new Random().nextInt();
static {
System.out.println("Parent static");
}
}
類的初始化時機
當java虛擬機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則卻不適用於接口。
初始化一個類時,並不會先初始化它實現的所有接口
初始化一個接口時,並不會先初始化它的父接口
因此,一個父接口並不會因爲子接口或者實現類的初始化而初始化。只有當程序首次使用特定接口的靜態變量時纔會導致該接口的初始化。
所有的java虛擬機實現必須在每個類或接口被java程序首次"主動使用"時才被初始化。