天天看點

java虛拟機類加載機制詳解

加載類的生命周期如下,加載的開始執行順序(注意是開始執行順序,而不是執行完再執行下一步,是交叉進行的)必須按照以下順序執行(解析和初始化某些情況會倒過來)。

java虛拟機類加載機制詳解

1.加載

加載分為以下三步:

a. 通過一個類的全限定名(比如: com.demo.test.java)擷取此類的二進制位元組流。(不一定直接加載一個class檔案,比如通過反射動态加載,通過jsp檔案生成class類,從壓縮包中讀取等)

b. 将此位元組流所代表的靜态存儲結構轉化成方法區(java8 以上放在元空間)的運作時資料結構。

c. 在記憶體中生成一個代表這個類的java.lang.class對象,作為這個類在方法區(java8 以上在元空間)各種資料(方法,變量等)的通路入口。

2.驗證

驗證過程是防止執行了有害代碼導緻系統崩潰(如果是通過編譯java類編譯的class檔案那麼在編譯時就會失敗,但是位元組流不止是來源于直接編譯java類生成了,還有其他的途徑生成,是以需要驗證位元組流的安全性)

a. 檔案格式驗證。比如驗證是否以magic number(magic number 用于标記一個檔案格式)0xCAFEBABY(java 的magic number格式)開頭,驗證主次版本是否在目前虛拟機處理範圍(允許執行低版本,不可執行超過目前版本的,比如jdk7虛拟機加載jdk8生成的位元組流)等等。

b. 中繼資料驗證。主要是對中繼資料進行語義分析,比如驗證是否有繼承父類(除了Objec類,其他類都應當有父類),驗證是否繼承了final繼承的類,重載方法是否符合規則,字段方法名是否沖突等等。

c. 位元組碼驗證。通過資料流和控制流分析整個程式的語義是否合法。比如是否直接将一個父類對象指派給一個子類資料類型,甚至是毫無相關的資料類型。比如保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。

d. 符号引用驗證,這個驗證發生在解析階段将符号引用轉化為直接引用。(符号引用: 比如一個類a引用了類b,由于編譯期并不知道類b實際記憶體位址,是以通過類似全限定名的常量來标記引用了類b,這個常量即為符号引用,方法和字段的符号引用也同理)。此驗證内容包括驗證通過該全限定名是否可以找到對應的類,在引用類中是否存在目前引用的方法和字段,符号引用的類,字段,方法的通路性(public,private,protected,default)是否可以被目前類通路。

3. 準備

準備階段是正式為類變量(被static修飾的變量)配置設定記憶體和設定變量初始值的階段。

如下定義的變量,在該階段value會被初始化被0,注意不是111而是0,因為通常情況下目前階段還沒有進行指派動作。

public static int value = 111;
           

但是如果是被final定義的屬性,那在該階段就會進行指派為111。

public static final int value = 111;
           

4.解析

解析階段是将符号引用替換成直接引用的過程。

a. 類或接口的解析,将代表符号引用的全限定名傳遞給類加載器去加載。

b. 字段解析,進行字段解析會先解析字段所屬的類或接口的符号引用,過程會先查類或接口本身有沒有包含此字段,如果找不到會從下而上找繼承的類或者實作的接口有沒有。

c. 類方法解析。類似字段的解析方式。

d. 接口方法解析。類似字段解析的方式。

5. 初始化

<clinit>() 方法(執行類變量即靜态變量的指派操作和靜态語句塊的語句 static{}),java類會先執行父類的clinit方法再執行子類,是以object類的clinit方法是最先執行的,接口不會去執行父類的clinit方法,多線程環境下同時初始化一個類,隻有一個執行,其他線程阻塞等待。

  • java類初始化三個特殊執行個體

a.通過子類引用父類的靜态字段不會觸發子類的類初始化

public class Test {
    public static void main(String[] args) {
        System.out.println(Test2.value);
    }
}
class Test1{
    public static int value =123;
    static {
        System.out.println("test1初始化了");
    }
}

class Test2 extends Test1{
    static {
        System.out.println("test2初始化了");
    }
}
           

java虛拟機類加載機制詳解

b.通過數組定義引用類,不會觸發此類的初始化

public class Test {
    public static void main(String[] args) {
        Test1[] test1 = new Test1[10];
    }
}
class Test1{
    public static int value =123;
    static {
        System.out.println("test1初始化了");
    }
}
           

結果如下,沒有任何輸出。

java虛拟機類加載機制詳解

c.調用另外一個類的定義的靜态常量,不會觸發被調用類的初始化

不會加載的原因是編譯階段通過常量傳播優化,調用Test1的常量會被優化成Test直接引用常量池中的該常量,是以實際上Test已經不存在Test1的符号引用入口了。

public class Test {
    public static void main(String[] args) {
        System.out.print(Test1.HELLO);
    }
}
class Test1{
    static final String HELLO = "hello";
    static {
        System.out.println("test1初始化了");
    }
}
           

結果如下。

java虛拟機類加載機制詳解

類加載器

1. bootstrap classLoader 啟動類加載器,用于加載jdk根目錄下的lib目錄中的jar檔案。

2. extension classLoader 擴充類加載器,用于加載jdk根目錄下的lib/ext/目錄中的jar檔案。

3. application classLoader 應用程式類加載器,用于加載classpath即使用者類路徑上的類,一般沒有自定義類加載器,那程式預設就是用這個加載器加載。

雙親委派模式

如果一個類加載器收到類加載的請求,不會先自己嘗試加載,而是先把這個請求交給父加載器加載,父加載器無法加載後,子加載器再去加載,加載關系圖如下。通過這個模式,可以避免系統出現同包同名的類,進而導緻應用程式出問題。

java虛拟機類加載機制詳解