1. 程式人生 > >類加載機制--淺談

類加載機制--淺談

雙親委派 我們 請求 new ble 如果 規範 數據結構 派生類

一、定義:

類加載(Class Loading)是一種機制,他描述的是將字節碼以文件形式加載到內存再經過連接、初始化後,最終形成可以被虛擬機直接使用的Java類型地過程。

Class Loading 包含了加載(Loading)、連接(Linking)、初始化(Initialization)三大部分,其中Linking又包含了三個部分:校驗(Verification)、準備 (Preparation)、解析(Resolution)。而一個類的生命周期只是在Class Loader的基礎上多了:使用(Using),卸載(Unloading)兩部分。

Class Loaders的組成:

技術分享圖片

二、加載階段

虛擬機需要完成以下3件事情:

1)通過一個類的全限定名來獲取定義此類的二進制字節流。

2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。

註意:

1.JVM在加載數組的時候加載的僅僅是數組的類型類(例如String[] 加載器只會加載String這個類型類),而數組的創建則由JVM直接完成。

這裏我們多問幾個為什麽:
2. JVM為什麽只加載數組的類型類:
我認為JVM這樣做的目的主要是為了節省時間,我們知道數組裏面裝的都是同一種類型的元素,JVM沒必要將一個重復的內容加載多次浪費時間。

3. N維數組怎麽加載:
如果是N維數組,類加載器會從最外層開始一層一層的遞歸加載,直到加載到非數組類型為止。
4. 引用類型與基本類型加載起來會不會有區別:
其實基本類型早已經在javac階段裝箱成封裝對象了,例如int會被裝箱成Integer,long裝箱成Long等等,所以是沒有區別的。

三、驗證

是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。但從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。
  2. 元數據驗證:對字節碼描述的信息進行語義分析(註意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
  3. 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
  4. 符號引用驗證:確保解析動作能正確執行。

四、準備階段

是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配

這個階段中有兩個容易產生混淆的概念需要強調一下,首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次,這裏所說的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義為:

public static int value=123;

那變量value在準備階段過後的初始值為0而不是123,因為這時候尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。表7-1列出了Java中所有基本數據類型的零值。

假設上面類變量value的定義變為:public static final int value=123;

編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為123。

五、解析階段

是虛擬機將常量池內的符號引用替換為直接引用的過程

六、初始化

在連接的準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員自己寫的邏輯去初始化類變量和其他資源,舉個例子如下:

    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }

在準備階段value1和value2都等於0;

在初始化階段value1和value2分別等於5和66;

  • 所有類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法,該方法只能在類加載的過程中由JVM調用;
  • 編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量;
  • 如果超類還沒有被初始化,那麽優先對超類初始化,但在<clinit>方法內部不會顯示調用超類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的超類<clinit>方法已經被執行。
  • JVM必須確保一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅只能允許其中一個線程對其執行初始化操作,其余線程必須等待,只有在活動線程執行完對類的初始化操作之後,才會通知正在等待的其他線程。(所以可以利用靜態內部類實現線程安全的單例模式)
  • 如果一個類沒有聲明任何的類變量,也沒有靜態代碼塊,那麽可以沒有類<clinit>方法;

何時觸發初始化

  1. 為一個類型創建一個新的對象實例時(比如new、反射、序列化)
  2. 調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)
  3. 調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操作時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化為一個編譯時常量表達式
  4. 調用JavaAPI中的反射方法時(比如調用java.lang.Class中的方法,或者java.lang.reflect包中其他類的方法)
  5. 初始化一個類的派生類時(Java虛擬機規範明確要求初始化一個類時,它的超類必須提前完成初始化操作,接口例外)
  6. JVM啟動包含main方法的啟動類時。

七、系統的類加載器

類加載器名稱

加載的範圍

啟動類加載器

Bootstrap ClassLoader

存放在<JAVA_HOME>\lib目錄中的,並且是虛擬機 識別的類庫加載到虛擬機內存中

擴展類加載器 Extension ClassLoader

存放在<JAVA_HOME>\lib\ext目錄中的所有類庫, 開發者可以直接使用;

應用程序加載器 Application ClassLoader

加載用戶類路徑上指定的類庫,開發者可以直 接使用,一般情況下這個就是程序中默認的類 加載器;

八、雙親委派機制

某個特定的類加載器在接到加載類的請求 時,首先將加載任務委托給父類加載器, 依次遞歸,如果父類加載器可以完成類加 載任務,就成功返回;只有父類加載器無 法完成此加載任務時,才自己去加載

雙親委派模型好處:Java類隨著它的類加載器一起具備了帶 有優先級的層次關系,保證java程序穩 定運行

九、示例代碼

public class SuperClazz {

    public SuperClazz(){
        System.out.println("我是父類構造方法");
    }

    static {
        System.out.println("我是父類靜態代碼塊");
    }

    public static int value = 123;

    public static final String STR = "hello world";

    public static final int WHAT = value;
}

public class SonClazz extends SuperClazz {

    public SonClazz(){
        System.out.println("我是子類構造方法");
    }
    static {
        System.out.println("我是子類靜態代碼塊");
    }
}
public class DemoTest {
public static void main(String args[]){ /*
        System.out.println(SonClazz.value);
輸出結果:
我是父類靜態代碼塊
123
結論:對於靜態字段,只有直接定義這個字段的類,才能被初始化。
*/
/*
SuperClazz[] aa = new SuperClazz[10];
輸出結果:(什麽也沒有輸出)
結論:只是聲明了一個數組形式的變量,類沒有初始化
*/

/*
System.out.println(SonClazz.STR);
輸出結果:hello world
結論:編譯期的傳播優化,因為是常量,所以就會直接編譯到了運行的類裏面,就不會再去定義的類裏面找了
,STR還是在常量池裏面的;
*/

     /**

      我是父類靜態代碼塊
      123

    */
//System.out.println(SonClazz.WHAT);


}
 
public class DemoTest2 {
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}
public class DemoTest2 {
    static
    {
        i=2;
//      System.out.println(i);
    }
    static int i=1;

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

輸出結果:1

類加載機制--淺談