ClassLoader類載入分析(一)

weixin_34146805發表於2018-03-08

一、什麼是Classloader

一個Java程式要想執行起來,首先需要經過編譯生成 .class檔案,然後建立一個執行環境(jvm)來載入位元組碼檔案到記憶體執行,而.class 檔案是怎樣被載入中jvm 中的就是Java Classloader所做的事情。


10175660-fc4f3b0b39421a8b.jpg
class檔案執行過程

那麼.class檔案什麼時候會被類載入器載入到jvm中執行那?比如執行new操作時候,當我們使用Class.forName(“包路徑+類名”),Class.forName(“包路徑+類名”,classloader),classloader.loadclass(“包路徑+類名”);時候就觸發了類載入器去類載入對應的路徑去查詢*.class,並建立Class物件。

類的載入過程

類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:

1、裝載

2-4、連結 -包括 【驗證、準備、解析】

5、初始化

6、使用

7、解除安裝

其中 連結(Link)又分3個步驟,如圖所示。類載入到解除安裝的生命週期流程圖如下:

10175660-21b86d1b16b11368.png
類載入過程

1) 裝載:查詢並載入類的二進位制資料(查詢和匯入Class檔案)

載入是類載入過程的第一個階段,在載入階段,虛擬機器需要完成以下三件事情:

1、通過一個類的全限定名來獲取其定義的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等)。

2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3、在Java堆中生成一個代表這個類的java.lang.Class物件,作為對方法區中這些資料的訪問入口。

相對於類載入的其他階段而言,載入階段(準確地說,是載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類載入器來完成載入,也可以自定義自己的類載入器來完成載入。

載入階段完成後,虛擬機器外部的 二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件訪問方法區中的這些資料。

2) 連結(分3個步驟

1、驗證:確保被載入的類的正確性

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

檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。

後設資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。

位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。

符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。

2、準備:為類的靜態變數分配記憶體,並將其初始化為預設值

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:

1、這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。

2、這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。

l例如在準備階段,為類變數(static修飾)在方法區中分配記憶體並設定初始值。
private static int var = 50;

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

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

3、解析:把類中的符號引用轉換為直接引用

解析階段是將常量池中的符號引用替換為直接引用的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫限定符7類符號引用進行.。符號引用和直接引用有什麼不同?
1、符號引用 :使用一組符號來描述所引用的目標,可以是任何形式的字面常量,定義在Class檔案格式中。
2、直接引用 :可以是直接指向目標的指標、相對偏移量或則能間接定位到目標的控制程式碼。

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

public class MuitiThreadInit {

    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、<clinit>方法對於類或介面不是必須的,如果一個類中沒有靜態程式碼塊,也沒有靜態變數的賦值操作,那麼編譯器不會生成<clinit>;
2、<clinit>方法與例項構造器不同,不需要顯式的呼叫父類的<clinit>方法,虛擬機器會保證父類的<clinit>優先執行;
3、為了防止多次執行<clinit>,虛擬機器會確保<clinit>方法在多執行緒環境下被正確的加鎖同步執行,如果有多個執行緒同時初始化一個類,那麼只有一個執行緒能夠執行<clinit>方法,其它執行緒進行阻塞等待,直到<clinit>執行完成。
4、注意:執行介面的<clinit>方法不需要先執行父介面的<clinit>,只有使用父介面中定義的變數時,才會執行。

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

1)遇到new, getstatic, putstatic, invokestatic 這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件、讀取或賦值一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)的時候,以及呼叫類的靜態方法。

2)使用java.lang.reflect包的方法對類進行反射呼叫時,如類沒有進行初始化,則需先觸發其初始化。

3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

4)虛擬機器啟動時,使用者需要指定一個啟動類(包含main()方法的類),jvm會先初始化這個主類。

5)當使用jdk1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行初始化,則需要先出觸發其初始化。

類的初始化步驟 / JVM初始化步驟:

1)如果這個類還沒有被載入和連結,那先進行載入和連結

2)假如這個類存在直接父類,並且這個類還沒有被初始化(注意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)

3 ) 假如類中存在初始化語句(如static變數和static塊),那就依次執行這些初始化語句。

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

public class Init {

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

}

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

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

輸出結果為:
Super init!
100
2、定義物件陣列,不會觸發該類的初始化。

public class Init {

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

}

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);  
    }  
}

輸出:
100
說明沒有觸發類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");

相關文章