類加載的過程

類加載的過程

類加載過程包括 5 個階段:加載、驗證、準備、解析和初始化。java

加載

加載的過程

「加載」是「類加載」過程的一個階段,不能混淆這兩個名詞。在加載階段,虛擬機須要完成 3 件事:數據庫

  • 經過類的全限定名獲取該類的二進制字節流。
  • 將二進制字節流所表明的靜態結構轉化爲方法區的運行時數據結構。
  • 在內存中建立一個表明該類的 java.lang.Class 對象,做爲方法區這個類的各類數據的訪問入口。

獲取二進制字節流

對於 Class 文件,虛擬機沒有指明要從哪裏獲取、怎樣獲取。除了直接從編譯好的 .class 文件中讀取,還有如下幾種方式:數組

  • 從 zip 包中讀取,如 jar、war等
  • 從網絡中獲取,如 Applect
  • 經過動態代理計數生成代理類的二進制字節流
  • 由 JSP 文件生成對應的 Class 類
  • 從數據庫中讀取,如 有些中間件服務器能夠選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發。

「非數組類」與「數組類」加載比較

  • 非數組類加載階段可使用系統提供的引導類加載器,也能夠由用戶自定義的類加載器完成,開發人員能夠經過定義本身的類加載器控制字節流的獲取方式(如重寫一個類加載器的 loadClass() 方法)
  • 數組類自己不經過類加載器建立,它是由 Java 虛擬機直接建立的,再由類加載器建立數組中的元素類。

注意事項

  • 虛擬機規範未規定 Class 對象的存儲位置,對於 HotSpot 虛擬機而言,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>() 方法。

相關文章
相關標籤/搜索