在java文件被編譯成class文件存儲爲二進制字節碼後,並不能直接使用,通過類加載,一個類才能夠被裝載進運行時內存並被使用。所以理解類加載機制才能讓咱們更深入地理解咱們編寫的java代碼是如何一步一步的編譯成class文件,到如何在內存中正確的使用的過程。複製代碼
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,而解析則不必定,解析主要目的是將符號引用轉換爲直接引用,它某些狀況能夠在初始化之後纔開始,這是爲了支持java的運行時綁定。java
關於加載何時開始,jvm規範中並無明確約束,由不一樣虛擬機本身把握,但對於初始化階段,虛擬機規範嚴格規定有且只有5種狀況必須對類進行初始化:數組
加載階段,虛擬機須要完成3件事:安全
上面說獲取二進制字節流,而沒有明確的說明是class文件中的字節流,由於還有其它獲取字節流的方式,例如從jar包中獲取、從網絡中獲取、動態代理運行時生成等。bash
加載階段與鏈接階段的部份內容是交叉進行的,如:一部分字節碼文件格式驗證動做。加載階段還沒有完成,鏈接階段可能已經開始了。網絡
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成如下4個階段的校驗動做:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。數據結構
這一階段目的是驗證二進制字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理,檢測內容包括如下幾點:jvm
這個階段是基於二進制字節流進行的,只有經過了這個階段的驗證,字節流纔會流入方法區中進行存儲,後面3個階段全是基於方法區的存儲結構進行的,不會再直接操做字節流。函數
這一階段主要對字節碼的描述信息進行語義分析,以保證其描述信息符合java語言規範,這階段的驗證點可能包括如下幾點:ui
這一階段目的主要目的是肯定程序語義是合法的、符合邏輯的。這個階段主要對類的字節碼進行校驗分析,保證該類的方法不會在運行時作出危害虛擬機安全的事:編碼
這一階段用來將符號引用轉換爲直接引用的時候,這個轉化將在解析階段中發生,符號引用驗證能夠看作是類對自身之外(常量池中各類符號引用)的信息進行匹配性校驗,一般須要校驗如下內容:
準備階段是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區分配。實例變量會在對象實例化的時候跟對象一塊兒在java堆中分配。這裏的初始值指的是一般狀況下的零值。假設一個類變量定義爲:
public static int a=123;
那麼變量a初始化的值是0而不是123。若是變量同時是final類型,那麼準備階段就會被賦值爲123,沒必要等到初始化階段再賦值。
解析階段是將虛擬機常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行。可能你們有疑問Class文件中哪有這麼多內容,其實上面也說了,是針對常量池。不論是CLass文件中的方法表仍是字段表,不能直接表示的內容,基本都會直接或間接存在常量池中,所以解析過程就是針對常量池中的數據類型進行解析的。
要把一個從未解析過的符號引用N解析爲一個類或接口的直接引用,虛擬機須要完成如下3個步驟:
對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用,若是解析這個類或符號引用的過程當中出現任何異常,都會致使字段符號引用解析的失敗。若是解析成功,這個字段對應的類或接口用C表示,接下來沿着A和它的父類/父接口尋找是否有這個字段,若是有會進行權限驗證,若是不具有權限則拋出異常。若是這個過程不出錯,則會在找到符合字段的時候返回這個字段的直接飲用,查找結束。
類方法解析首先也要首先解析出類方法表class_index項中索引的方法所屬的類或接口的符號引用,解析成功用C表示。
最後若是查找成功返回了直接引用,還要對這個方法進行權限驗證,若是不具有權限,則會拋出異常。
接口方法須要先解析出接口方法表的class_index 項中索引的方法所屬的類或接口的符號引用。
類初始化是類加載過程的最後一步。前面的類加載過程當中,除了加載階段能夠自定義類加載器干預以外,其他動做徹底由虛擬機主導。到了初始化階段,才真正開始執行java代碼。
咱們知道,在前面的準備階段,已經對類變量分配過內存並設置初始值。在初始化階段,則是爲類變量或其它資源設置程序中聲明的值。注意這裏仍然是類變量,不包括實例變量。或者明確的說,這一階段,是執行static關鍵字修飾的變量或代碼塊。本質上,初始化是執行類構造器
<client>方法的過程。
<client>方法是由編譯器自動收集類中全部類變量的賦值動做和靜態代碼塊中的語句合併產生的。編譯器收集的順序是有語句在資源文件中出險的順序所決定的。
所以平時可能會遇到這種問題:以下代碼
public class Client {
private static Client client = new Client();
public static int a;
public static int b = 0;
private Client() {
a++;
b++;
}
public static Client getInstance() {
return client;
}
public static void main(String[] args) {
Client instance = Client.getInstance();
System.out.println("a= " + Client.a);
System.out.println("b= " + Client.b);
}
}複製代碼
輸出結果是
a= 1
b= 0複製代碼
可能有人問爲何,其實把類加載的過程邏輯理清楚,也不是問題。咱們知道在類加載的準備階段會給類變量分配內存和賦初始值。在外部調用Client.getInstance()時,由於以前類沒有被加載過,會引起類加載,到了準備階段就會給類變量賦初始值。賦值順序同一個類中是按聲明的順序,也就是
client=null;
a=0;
b=0複製代碼
而後解析完開始初始化,按程序聲明的值給類變量賦值。首先執行clinet=new Client(),其實關鍵就是這裏new的過程會調用構造函數,調用完後
a=1;
b=1;複製代碼
接着繼續初始化,a只是聲明沒有賦值,因此沒有任何操做,b聲明且賦值爲0,因此初始化完成後
a=1;
b=0;複製代碼