1. 程式人生 > >JVM類載入過程

JVM類載入過程

Java原始碼被編譯成class位元組碼,最終需要載入到虛擬機器中才能執行。整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。

載入

1、通過一個類的全限定名獲取描述此類的二進位制位元組流;
2、將這個位元組流所代表的靜態儲存結構儲存為方法區的執行時資料結構;
3、在java堆中生成一個代表這個類的java.lang.Class物件,作為訪問方法區的入口;

虛擬機器設計團隊把載入動作放到JVM外部實現,以便讓應用程式決定如何獲取所需的類,實現這個動作的程式碼稱為“類載入器”,JVM提供了3種類載入器:

1、啟動類載入器(Bootstrap ClassLoader):負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。

2、擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。

3、應用程式類載入器(Application ClassLoader):負責載入使用者路徑(classpath)上的類庫。

JVM基於上述類載入器,通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。

雙親委派模型工作過程:當一個類載入器收到類載入任務,優先交給其父類載入器去完成,因此最終載入任務都會傳遞到頂層的啟動類載入器,只有當父類載入器無法完成載入任務時,才會嘗試執行載入任務。

雙親委派模型有什麼好處?

比如位於rt.jar包中的類java.lang.Object,無論哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,確保了Object類在各種載入器環境中都是同一個類。

驗證

為了確保Class檔案符合當前虛擬機器要求,需要對其位元組流資料進行驗證,主要包括格式驗證、元資料驗證、位元組碼驗證和符號引用驗證。

  1. 格式驗證

驗證位元組流是否符合class檔案格式的規範,並且能被當前虛擬機器處理,如是否以魔數0xCAFEBABE開頭、主次版本號是否在當前虛擬機器處理範圍內、常量池是否有不支援的常量型別等。只有經過格式驗證的位元組流,才會儲存到方法區的資料結構,剩餘3個驗證都基於方法區的資料進行。

  1. 元資料驗證

對位元組碼描述的資料進行語義分析,以保證符合Java語言規範,如是否繼承了final修飾的類、是否實現了父類的抽象方法、是否覆蓋了父類的final方法或final欄位等。

  1. 位元組碼驗證

對類的方法體進行分析,確保在方法執行時不會有危害虛擬機器的事件發生,如保證運算元棧的資料型別和指令程式碼序列的匹配、保證跳轉指令的正確性、保證型別轉換的有效性等。

  1. 符號引用驗證

為了確保後續的解析動作能夠正常執行,對符號引用進行驗證,如通過字串描述的全限定名是都能找到對應的類、在指定類中是否存在符合方法的欄位描述符等。
準備

在準備階段,為類變數(static修飾)在方法區中分配記憶體並設定初始值。

private static int var = 100;

準備階段完成後,var 值為0,而不是100。在初始化階段,才會把100賦值給val,但是有個特殊情況:

private static final int VAL= 100;

在編譯階段會為VAL生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將VAL賦值為100。

解析

解析階段是將常量池中的符號引用替換為直接引用的過程,符號引用和直接引用有什麼不同?

1、符號引用使用一組符號來描述所引用的目標,可以是任何形式的字面常量,定義在Class檔案格式中。

2、直接引用可以是直接指向目標的指標、相對偏移量或則能間接定位到目標的控制代碼。

初始化

初始化階段是執行類構造器方法的過程,方法由類變數的賦值動作和靜態語句塊按照在原始檔出現的順序合併而成,該合併操作由編譯器完成。

private static int value = 100;
    static int a = 100;
    static int b = 100;
    static int c;

    static {
        c = a + b;
        System.out.println("it only run once");
    }

1、方法對於類或介面不是必須的,如果一個類中沒有靜態程式碼塊,也沒有靜態變數的賦值操作,那麼編譯器不會生成;

2、方法與例項構造器不同,不需要顯式的呼叫父類的方法,虛擬機器會保證父類的優先執行;

3、為了防止多次執行,虛擬機器會確保方法在多執行緒環境下被正確的加鎖同步執行,如果有多個執行緒同時初始化一個類,那麼只有一個執行緒能夠執行方法,其它執行緒進行阻塞等待,直到執行完成。

4、注意:執行介面的方法不需要先執行父介面的,只有使用父介面中定義的變數時,才會執行。

類初始化場景

虛擬機器中嚴格規定了有且只有5種情況必須對類進行初始化。

執行new、getstatic、putstatic和invokestatic指令;

使用reflect對類進行反射呼叫;

初始化一個類的時候,父類還沒有初始化,會事先初始化父類;

啟動虛擬機器時,需要初始化包含main方法的類;

在JDK1.7中,如果java.lang.invoke.MethodHandler例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼對應的類沒有進行初始化;

以下幾種情況,不會觸發類初始化

1、通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。

class Parent {
    static int a = 100;
    static {
        System.out.println("parent init!");
    }
}

class Child extends Parent {
    static {
        System.out.println("child init!");
    }
}

public class Init{  
    public static void main(String[] args){  
        System.out.println(Child.a);  
    }  
}

輸出結果為:parent init!

2、定義物件陣列,不會觸發該類的初始化。

public class Init{  
    public static void main(String[] args){  
        Parent[] parents = new Parent[10];
    }  
}

無輸出,說明沒有觸發類Parent的初始化,但是這段程式碼做了什麼?先看看生成的位元組碼指令
anewarray指令為新陣列分配空間,並觸發[Lcom.ctrip.ttd.whywhy.Parent類的初始化,這個類由虛擬機器自動生成。

3、常量在編譯期間會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。

class Const {
    static final int A = 100;
    static {
        System.out.println("Const init");
    }
}

public class Init{  
    public static void main(String[] args){  
        System.out.println(Const.A);  
    }  
}

無輸出,說明沒有觸發類Const的初始化,在編譯階段,Const類中常量A的值100儲存到Init類的常量池中,這兩個類在編譯成class檔案之後就沒有聯絡了。

4、通過類名獲取Class物件,不會觸發類的初始化。

public class test {
   public static void main(String[] args) throws ClassNotFoundException {
        Class c_dog = Dog.class;
        Class clazz = Class.forName("zzzzzz.Cat");
    }
}

class Cat {
    private String name;
    private int age;
    static {
        System.out.println("Cat is load");
    }
}

class Dog {
    private String name;
    private int age;
    static {
        System.out.println("Dog is load");
    }
}

執行結果:Cat is load,所以通過Dog.class並不會觸發Dog類的初始化動作。

5、通過Class.forName載入指定類時,如果指定引數initialize為false時,也不會觸發類初始化,其實這個引數是告訴虛擬機器,是否要對類進行初始化。

public class test {
   public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = Class.forName("zzzzzz.Cat", false, Cat.class.getClassLoader());
    }
}
class Cat {
    private String name;
    private int age;
    static {
        System.out.println("Cat is load");
    }
}

6、通過ClassLoader預設的loadClass方法,也不會觸發初始化動作

new ClassLoader(){}.loadClass("zzzzzz.Cat");