類從被加載到虛擬機,到被卸載。其整個生命週期包括以上幾個階段:加載 (Loading)、驗證 (Verification)、準備 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using)、卸載 (Unloading)
。java
其中類加載過程包括加載 (Loading)、驗證 (Verification)、準備 (Preparation)、解析 (Resolution)、初始化 (Initialization)
五個階段。除了解析 (Resolution)
階段順序不肯定之外,其餘四個階段是按順序開始的。數據庫
解析階段可能在初始化階段以後開始,其目的是爲了支持Java的動態綁定,好比多態(調用父類方法實際執行的是子類覆蓋的方法)。安全
在加載階段,虛擬機完成如下三件事情:網絡
java.lang.Class
對象,做爲對方法區中這些數據的訪問入口。在加載階段咱們能夠實現本身的ClassLoader
,從而能夠動態的建立符合特定化需求的類,或者是能夠從特定的數據源 (網絡、文件系統、數據庫等等) 獲取class文件。數據結構
大體的類加載器層級結構以下,多線程
啓動類加載器 (Bootstrap ClassLoader),負責加載 JDK\jre\lib 下
,或被-Xbootclasspath
參數指定的路徑中的,而且能被虛擬機識別的類庫(如rt.jar,全部java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器沒法被Java程序直接引用。佈局
擴展類加載器 (Extension ClassLoader),負責加載JDK\jre\lib\ext
目錄中,或者由java.ext.dirs
系統變量指定的路徑中的全部類庫(如javax.*開頭的類),開發者能夠直接使用擴展類加載器。spa
應用程序類加載器 (Application ClassLoader), 負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器。若是沒有自定義的類加載器,通常這就是程序中默認的類加載器。線程
如上圖所示的這種層級結構成爲Java類加載器的雙親委派模型。當前加載器上層的叫作父加載器,但他們的關係不是靠繼承來實現,而是使用組合的方式。設計
若是一個類加載器收到加載類的請求,首先將請求委託給父加載器,依次向上層請求,直到頂端的啓動加載器,此時只有當該加載器找不到對應的類時,纔會讓子類去加載,直到找到該類。
Java設計者提出的這種約束模型,
例如要請求加載java.lang.Object
類,最終全部的加載器都會委託啓動加載器去加載,從而保證了不管是哪個加載器想要加載Object
類,最終都指向同一個類。
驗證是爲了確保Class文件中的字節流符合虛擬機的要求,而且不會損害虛擬機安全。
驗證大體分爲四個階段,
主要爲類變量分配內存並設置變量初始值,都在方法區中進行分配。
注意要點:
舉個例子,以下
public static int number = 6;
複製代碼
變量number
在準備階段後的值爲0,而不是6。由於此時尚未開始執行Java方法,而將變量number
賦值爲6是在程序編譯後,執行putstatic
指令,存放於類的構造器<clinit>()
方法中,因此number
賦值爲6的操做將在初始化階段進行。
但若是上述變量前加上final
關鍵字,則會在編譯期就將其結果放入常量池中,即準備階段後此時number
值爲6.
該階段虛擬機完成將常量池中符號引用轉化爲直接引用的過程。其中,
解析主要針對類或接口、字段、類方法、接口方法四類符號引用,他們和常量池中的類型對應關係以下表,
符號引用 | 常量類型(常量池) |
---|---|
類或接口 | CONSTANT_Class_info |
字段 | CONSTANT_Fieldref_info |
類方法 | CONSTANT_Methodref_info |
接口方法 | CONSTANT_InterfaceMethodref_info |
此階段開始執行類中的Java代碼,主要執行的是類構造器<clinit>()
方法。
<clinit>()
是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序由 語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它以前的類變量,定義在它以後的類 變量只能賦值,不能訪問。例如如下代碼:
public class Test {
static {
i = 0; // 給變量賦值能夠正常編譯經過
System.out.print(i); // 這句編譯器會提示「非法向前引用」
}
static int i = 1;
}
複製代碼
因爲父類的 <clinit>()
方法先執行,也就意味着父類中定義的靜態語句塊的執行要優先於子類。例如如下代碼:
static 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); // 2
}
複製代碼
接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>()
方法。但 接口與類不一樣的是,執行接口的 <clinit>()
方法不須要先執行父接口的 <clinit>()
方法。只有當父接口中定義的變量使 用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()
方法。
虛擬機會保證一個類的 <clinit>()
方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會 有一個線程執行這個類的 <clinit>()
方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>()
方法完畢。若是在一 個類的 <clinit>()
方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。