虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
2.類加載時機
- 加載:Java虛擬機規範並沒有嚴格規定,主流虛擬機是懶加載。
- 連接:加載開始之後,加載完後,連接結束。
- 初始化:遇到new、getStatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。使用Java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。當一個初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主類。
- 使用:用戶使用時。
- 卸載:代碼中再不使用時。
- 不被初始化的例子:
通過數組定義來引用類;
調用類的常量;
3.加載
通過一個類的全限定名來獲取定義此類的二進制流。
將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
在內存中生成一個代表這個類的Class對象(方法區),作爲這個類的各種數據訪問入口。
源:
文件
Class文件
Jar文件
網絡
計算生成一個二進制流
$Proxy
由其他文件生成
Jsp
數據庫
4.驗證
驗證是連接的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
- 文件格式驗證(文件是否是.class等);
- 元數據驗證(是否有父類,父類是否是final的等);
- 字節碼驗證(分析控制流,分析程序的執行流是否符合語意);
符號引用驗證(描述全限定名是否能找到該類,在指定類中是否有方法或者字段的描述,確保解析的動作能夠正常執行);
5.準備
準備階段正式爲類變量分配內存並設置變量的初始值。這些變量使用的內存都將在方法區中進行分配。這裏的初始值並非我們指定的值,而是其默認值。但是如果被final修飾,那麼在這個過程中,常量值會一同被指定。
6.解析
解析階段是將常量池中的符號引用替換爲直接引用的過程。在進行解析之前需要對符號引用進行解析,不同虛擬機實現可以根據需要判斷到底在類被加載的時候對常量池的符號進行解析(也就是初始化之前),還是等到一個符號引用被使用之前進行解析(也就是在初始化之後)。
如果一個符號引用進行多次解析請求,虛擬機中除了invokedynamic指令外,虛擬機可以對第一次解析的結果進行緩存(在運行時常量池中記錄引用,並把常量標識爲解析狀態),這樣就避免了一個符號引用的多次解析。解析動作主要針對的是類或者接口、字段、類方法、方法類型、方法句柄和調用點限定符7類符號引用。
- 類或者接口解析
如果該符號引用是一個數組類型,並且該數組的元素是對象。我們知道符號引用是存在方法區常量池中的,該符號引用的描述符會類似[java/lang/Integer的形式,將會按照上面的規則進行加載數組元素類型,如果描述符入前面假設的形式,需要加載的元素類型就是java.lang.Integer,接着由虛擬機將會生成一個代表此數組對象的直接引用。
如果上面的步驟沒有出現異常,那麼該符號引用已經在虛擬機中產生了一個直接引用,但是在解析完成之前需要對符號引用進行驗證,主要是確認當前調用這個符號引用的類是否具有訪問權限,如果沒有訪問權限將拋出java.lang.IllegalAccess異常。
- 字段解析
如果該字段符號引用就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,解析結束。
否則,如果該符號的類實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它分父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,那麼就直接返回這個字段的直接引用,解析結束。
否則,如果該符號所在類不是Object類的話,將會按照繼承關係從下往上遞歸搜索其父類,如果父類中包含了簡單名稱和字段描述符都相匹配的字段,那麼直接返回這個字段的直接引用,解析結束。
否則,解析失敗,拋出java.lang.NoSuchFieldError異常。
如果最終返回了這個字段的直接引用,就進行權限驗證,如果發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。
- 類方法解析
類方法和接口方法的符號引用是分開的,所以如果在類方法表中發現class_index(類中的符號引用)的索引是一個接口,那麼會拋出java.lang.IncompatibleClassChangeError的異常。
如果class_index的索引確實是一個類,那麼在該類中查找是否有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就返回這個方法的直接引用,查找結束。
否則,在該類的父類中遞歸查找是否具有簡單名稱和描述符都與目標字段相匹配的方法,如果有,則返回這個字段的直接引用,查找結束。
否則,在這個類的接口以及它的父接口中遞歸查找,如果找到的話就說明這個方法是一個抽象類,查找結束返回java.lang.AbstractMethodError異常。
否則,查找失敗,拋出java.lang.NoSuchMethodError。
如果最終返回了直接引用,還需要對該符號引用進行權限驗證,如果沒有訪問權限,就拋出java.lang.IllegalAccessError異常。
- 接口方法解析
否則,在該接口方法的所屬的接口中查找是否具有簡單名和描述符都與目標字段相匹配的方法,如果有的話就直接返回這個方法的直接引用。
否則,再改接口以及其父接口中查找,直到Object類,如果找到則直接返回這個方法的直接引用。
否則,查找失敗。
接口的所有方法都是public,所以不存在訪問權限問題。
7.初始化
初始化是類加載的最後一步。
初始化是執行<clinit>()方法的過程。
類初始化階段是類加載的最後一步,前面類加載的過程除了在加載階段用用應用程序可以通過自定義代類加載器參與之外,其餘動作完全由虛擬機主導與控制。到了初始化階段,纔是真正執行類中定義的Java程序代碼。
在準備階段,變量已經被賦過一次系統要求的初始值,而在初始化階段,則根據開發者通過程序控制制定的主觀計劃去初始化類變量和其他資源。
先來看一段代碼
public class Demo {
static {
i = 0;
System.out.println(i);
}
static int i = 1;
}
上面這段代碼變量的賦值語句可以通過編譯,下面的輸出貶義不過。<clinit>()方法是由編譯器自動收集類中的所有變量的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的語句塊中可以賦值,但是不能訪問。
再看一段代碼
public class Parent {
public static int A = 1;
static {
A = 2;
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
子類的<clinit>()在執行之前,虛擬機保證父類的先執行完畢,因此在賦值前父類static已經執行,因此結果爲2。接口中也有變量要賦值,也會生成<clinit>(),但不需要先執行父類的<clinit>()方法,只有父接口中定義的變量使用時纔會初始化。
如果多個線程同時初始化一個類,只有一個線程會執行這個類的<clinit>()方法,其他線程等待執行完畢,如果方法執行時間過長,那麼就會造成多個線程阻塞。