類加載過程包括 5 個階段:加載、驗證、準備、解析和初始化。java
「加載」是「類加載」過程的一個階段,不能混淆這兩個名詞。在加載階段,虛擬機須要完成 3 件事:數據庫
對於 Class 文件,虛擬機沒有指明要從哪裏獲取、怎樣獲取。除了直接從編譯好的 .class 文件中讀取,還有如下幾種方式:數組
驗證階段確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。安全
驗證字節流是否符合 Class 文件格式的規範,而且能被當前版本的虛擬機處理,驗證點以下:
服務器
- 是否以魔數 0XCAFEBABE 開頭 - 主次版本號是否在當前虛擬機處理範圍內 - 常量池是否有不被支持的常量類型 - 指向常量的索引值是否指向了不存在的常量 - CONSTANT_Utf8_info 型的常量是否有不符合 UTF8 編碼的數據 - ......
對字節碼描述信息進行語義分析,確保其符合 Java 語法規範。網絡
本階段是驗證過程當中最複雜的一個階段,是對方法體進行語義分析,保證方法在運行時不會出現危害虛擬機的事件。數據結構
本階段發生在解析階段,確保解析正常執行。多線程
準備階段是正式爲類變量(或稱「靜態成員變量」)分配內存並設置初始值的階段。這些變量(不包括實例變量)所使用的內存都在方法區中進行分配。編碼
初始值「一般狀況下」是數據類型的零值(0, null...),假設一個類變量的定義爲:線程
public static int value = 123;
那麼變量 value 在準備階段事後的初始值爲 0 而不是 123,由於這時候還沒有開始執行任何 Java 方法。
存在「特殊狀況」:若是類字段的字段屬性表中存在 ConstantValue 屬性,那麼在準備階段 value 就會被初始化爲 ConstantValue 屬性所指定的值,假設上面類變量 value 的定義變爲:
public static final int value = 123;
那麼在準備階段虛擬機會根據 ConstantValue 的設置將 value 賦值爲 123。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
類初始化階段是類加載過程的最後一步,是執行類構造器 <clinit>() 方法的過程。
<clinit>() 方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static {} 塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。
靜態語句塊中只能訪問定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊中<font color="#C40000">能夠賦值,但不能訪問</font>。以下方代碼所示:
public class Test { static { i = 0; // 給變量賦值能夠正常編譯經過 System.out.println(i); // 這句編譯器會提示「非法向前引用」 } static int i = 1; }
<clinit>() 方法不須要顯式調用父類構造器,虛擬機會保證在子類的 <clinit>() 方法執行以前,父類的 <clinit>() 方法已經執行完畢。
因爲父類的 <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>() 方法。