類加載讀書筆記

類加載的流程

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

  或者面試

  加載->驗證->準備->初始化->解析數組

類加載的時機

  什麼狀況下須要開始類加載過程的第一個階段:加載?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 虛擬機》,我想強調的有兩點bash

  一、以上五點是指類被初始化(參考初始化階段)的時機,而不是被加載的時機。數據結構

  二、加載、驗證、準備這三個階段能夠先進行,而初始化能夠遲遲不進行直到必要的時候編輯器

類加載過程的詳細描述

一、加載

  比較容易理解,其實就是將 .class 文件加載到內存,並生成一個 Class 對象來方便管理,具體流程以下:佈局

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

  簡化流程圖以下所示: spa

  須要注意的是若是是數組會涉及到兩個類,好比:

String[] strArr = new String[10];
複製代碼

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

  • 數組類:無需加載,直接由虛擬機建立,由 newarray 指令觸發,該類中包含數組應有的屬性和方法,方便對數組自己進行管理。
  • String 類:數組中的元素類,普通類的加載方式。

二、驗證

  驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證大體上會完成下面四個階段的檢驗工做:

  1. 文件格式驗證:是否以魔數開頭、主次版本號是否在當前虛擬機處理範圍內、常量池的各類索引值是否指向不存在的常量或不符合類型的常量等。主要是對文件格式的驗證,以保證能被虛擬機解析。
  2. 元數據驗證:是否有父類、父類是否繼承了不容許繼承的類、非抽象類是否實現了父類或接口中要求的全部方法、類中的字段方法是否與父類衝突等。主要對元數據或者說語法上的驗證,跟咱們在 Java 代碼中編輯器作的事情很像。
  3. 字節碼驗證:保證跳轉指令不會跳轉到方法體之外的指令上、保證類型轉換是有效的等。主要是從字節碼的層面去驗證,保證程序語義的合法和符合邏輯。
  4. 符號驗證:符號引用中經過字符串描述的全額限定名是否能找到對應的類、在指定類中是否存在符號方法的字段描述符以及簡單名稱所描述的方法和字段等。符號引用驗證的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將會拋出異常。   驗證階段重要但並不必定是必要的,若是全部代碼已經被反覆確認和驗證。能夠經過 -Xverify:none 關閉驗證以減小類的加載時間。    驗證階段雖然東西比較多,但要作的事仍是比較容易理解的,無非就是一些 if-else 的判斷罷了。

三、準備

  準備階段也比較簡單,搞清楚如下幾點便可:

  1. 準備階段是爲類變量(被 static 修飾的變量)分配內存和賦初始值的階段。
  2. 類變量的內存分配在方法區,而實例變量會在對象實例化時隨着對應一塊兒分配在堆中。
  3. 與初始化階段不一樣,類變量賦初始值只是賦相似 0 值的操做。如public static int value=123;只是將 value 賦值爲 0 而不是 123。

  注:各類Java 各類基本類型 0 值對照表以下

 4. 但須要注意的是, 類常量public static final int value=123;則 value 在準備階段即被賦值爲 123,至於緣由也是顯而易見的

四、解析

  解析是將符號引用轉化成直接引用的階段。符號引用和直接引用在《深刻理解 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 即直接引用,直接引用也能夠是內存地址的偏移量或者間接句柄等,只要能在內存中經過該引用訪問到目標便可。因爲直接引用是經過虛擬機將符號引用翻譯成內存地址的,因此是虛擬機有關的。

   經過這個例子能夠簡單的理解符號引用和直接引用以及它們的關係,從而明白解析階段的做用。

五、初始化

  類初始化階段是類加載過程的最後一步,該階段才真正開始執行類中定義的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);  //入口
    }
}
複製代碼
相關文章
相關標籤/搜索