類載入讀書筆記

水目沾發表於2019-05-07

類載入的流程

  載入->驗證->準備->解析->初始化

  或者

  載入->驗證->準備->初始化->解析

類載入的時機

  什麼情況下需要開始類載入過程的第一個階段:載入?Java虛擬機器規範中並沒有進行強制約束,這點可以交給虛擬機器的具體實現來自由把握。但是對於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行**“初始化”**(而載入、驗證、準備自然需要在此之前開始): 有且僅有以下五種情況:

  1. 遇到 new、getstatic、putstatic 和invokestatic 這四條位元組碼指令的時候。四條位元組碼分別對應新建物件、讀取或者設定一個類的靜態變數以及呼叫一個類的靜態方法。
  2. 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候。
  3. 當初時候一個類發現其父類還沒有進行初始化,則需要對其父類進行初始化。
  4. 虛擬機器啟動的時候,虛擬機器會初始化 main 所在的類。
  5. 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

  這段文字完完全全來自於《深入理解 Java 虛擬機器》,我想強調的有兩點

  1、以上五點是指類被初始化(參考初始化階段)的時機,而不是被載入的時機。

  2、載入、驗證、準備這三個階段可以先進行,而可以遲遲不進行初始化直到必要的時候

類載入過程的詳細描述

1、載入

  • 通過一個類的全額限定名來獲取定義此類的二進位制位元組流。
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區這個類的各種資料的訪問入口。 簡化流程圖如下所示:

類載入讀書筆記
  需要注意的是如果是陣列資料,比如:

String[] strArr = new String[10];
複製程式碼

  這個語句會涉及到兩個類:

  • 陣列類:由虛擬機器直接建立,由 newarray 指令觸發,該類中包含陣列應有的屬性和方法。
  • String 類:普通類的載入方式。

2、驗證

  驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證大致上會完成下面四個階段的檢驗工作:

  1. 檔案格式驗證:是否以魔數開頭、主次版本號是否在當前虛擬機器處理範圍內、常量池的各種索引值是否指向不存在的常量或不符合型別的常量等。主要是對檔案格式的驗證,以保證能被虛擬機器解析。
  2. 後設資料驗證:是否有父類、父類是否繼承了不允許繼承的類、非抽象類是否實現了父類或介面中要求的所有方法、類中的欄位方法是否與父類衝突等。主要對後設資料或者說語法上的驗證,跟我們在 Java 程式碼中編輯器做的事情很像。
  3. 位元組碼驗證:保證跳轉指令不會跳轉到方法體以外的指令上、保證型別轉換是有效的等。主要是從位元組碼的層面去驗證,保證程式語義的合法和符合邏輯。
  4. 符號驗證:符號引用中通過字串描述的全額限定名是否能找到對應的類、在指定類中是否存在符號方法的欄位描述符以及簡單名稱所描述的方法和欄位等。符號引用驗證的目的是確保解析動作能正常執行,如果無法通過符號引用驗證,那麼將會丟擲異常。   驗證階段重要但並不一定是必要的,如果所有程式碼已經被反覆確認和驗證。可以通過 -Xverify:none 關閉驗證以減少類的載入時間。

3、準備

  準備階段比較簡單,搞清楚以下幾點即可:

  1. 準備階段是為**類變數(被 static 修飾的變數)**分配記憶體和賦初始值的階段。
  2. 類變數的記憶體分配在方法區,而例項變數會在物件例項化時隨著對應一起分配在堆中。
  3. 與初始化階段不同,類變數賦初始值只是賦類似 0 值的操作。如public static int value=123;只是將 value 賦值為 0 而不是 123。

  注:各種Java 各種基本型別 0 值對照表如下

類載入讀書筆記
4. 但需要注意的是,類常量public static final int value=123;則 value 在準備階段即被賦值為 123,至於原因也是顯而易見的

4、解析

  解析是將符號引用轉化成直接引用的階段。符號引用和直接引用在《深入理解 Java 虛擬機器》中的定義如下:

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在記憶體中存在。

  上面的定義看的一臉懵逼,我舉個例子來理解上面一大段文字。

public class Test {
    public static final mian(String[] args) {
        System.out.println("Hello, /word!");    //1
    }
}
複製程式碼

  如上可以看到 Test 是一個非常簡單的類,Test 類中呼叫了 System.out.pintln 介面向螢幕上輸出 Hello, world 。該類的 .class 檔案中的字串常量中(層層轉換後)肯定會包含以下的一個常量:

invokevirtual java/io/PrintStream.println:(Ljava/lang/String;)V
複製程式碼

  很明顯可以看出以上是一個方法的呼叫(即 System.out.println())指令:其中 java/io/PrintStream 是方法所在的類,println 是方法名,Ljava/lang/String 表示引數型別,這樣就能唯一確定需要呼叫的方法,而java/io/PrintStream.println:(Ljava/lang/String;)V即我們的所說的符號引用。同樣如果訪問其他類、介面、類的欄位、類的方法、介面的方法等,都需要在 .class 檔案中通過符號引用指定。由於只是一個字串,所以是虛擬機器無關的。    Test 類的.class 檔案最終是要被載入到虛擬機器記憶體中才能被執行的,而在記憶體中方法的訪問是通過地址實現的。所以需要將符號引用java/io/PrintStream.println:(Ljava/lang/String;)V 轉換成一個指向方法在記憶體中具體地址的指標,.class 中指令變成類似如下形式:

invokevirtual 0xfe1886d2;(配圖如下)
複製程式碼

類載入讀書筆記
   其中 0xfe1886d2 即直接引用,直接引用也可以是記憶體地址的偏移量或者間接控制程式碼等,只要能在記憶體中通過該引用訪問到目標即可。通過這個例子可以簡單的理解符號引用和直接引用以及它們的關係,從而明白解析階段的作用。

5、初始化

  類初始化階段是類載入過程的最後一步,該階段才真正開始執行類中定義的Java程式程式碼(或者說是位元組碼)。

  類初始化過程即執行 <clinit>()方法的過程,而 <clinit>() 方法又是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的。需要注意的是:

  • 靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。

  初始化的時機在類載入載入時機一章節已經列出,是為了區分類載入時機和類初始化時機。只有清晰的理解需要初始化的各種場景才能真正把握初始化的含義。

總結

  類的載入很多人似懂非懂,特別是解析和初始化這裡,總結為一句即:準備和初始化階段完成了 static 變數的記憶體分配和賦值以及 static 程式碼塊中程式碼的執行。出道網上的面試題,在不百度的情況下寫出最後輸出的字串試試,如果你能正確寫出答案那大概率已經弄到了解析和初始化階段,面試題如下:

class Grandpa
{
    static
    {
        System.out.println("爺爺在靜態程式碼塊");
    }
}    
class Father extends Grandpa
{
    static
    {
        System.out.println("爸爸在靜態程式碼塊");
    }

    public static int factor = 25;

    public Father()
    {
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
    static 
    {
        System.out.println("兒子在靜態程式碼塊");
    }

    public Son()
    {
        System.out.println("我是兒子~");
    }
}
public class InitializationDemo
{
    public static void main(String[] args)
    {
        System.out.println("爸爸的歲數:" + Son.factor);  //入口
    }
}
複製程式碼

相關文章