[深入理解Java虛擬機器]第七章 類載入的時機

Coding-lover發表於2015-10-23

概述

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始 化 ,最終形成可以被虛擬機器直接使用的Java類 型 ,這就是虛擬機器的類載入機制。

與那些在編譯時需要進行連線工作的語言不同,在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的,這種策略雖然會令類載入時稍微增加一些效能開銷 ,但是會為Java應用程式提供高度的靈活性,Java裡天生可以動態擴充套件的語言特性就是依 賴執行期動態載入和動態連線這個特點實現的。例如 ,如果編寫一個面向介面的應用程式, 可以等到執行時再指定其實際的實現類;使用者可以通過Java預定義的和自定義類載入器,讓一個本地的應用程式可以在執行時從網路或其他地方載入一個二進位制流作為程式程式碼的一部分,這種組裝應用程式的方式目前已廣泛應用於Java程式之中。從最基礎的Applet、JSP到相對複雜的OSGi技術 ,都使用了Java語言執行期類載入的特性。

類載入的時機

類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括:載入 ( Loading ) 、驗證( Verification ) 、準備( Preparation ) 、解析( Resolution )、初始化( Initialization ) 、使用( Using ) 和解除安裝( Unloading ) 7個階段。其中驗證、準備、解析3個部分統稱為連線( Linking ) ,這7個階段的發生順序如圖7-1所示。

圖7-1中 ,載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。注意 ,這裡筆者寫的是按部就班地“開始” ,而不是按部就班地“進行”或“完成”,強調這點是因為這些階段通常都是互相交叉地混合式進行的,通常會在一個階段執行的過程中呼叫、啟用另外一個階段。

什麼情況下需要開始類載入過程的第一個階段:載入?Java虛擬機器規範中並沒有進行強制約束 ,這點可以交給虛擬機器的具體實現來自由把握。但是對於初始化階段,虛擬機器規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化” (而載入、驗證、準備自然需要在此之前開始):

  • 1 ) 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。
  • 2 ) 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化, 則需要先觸發其初始化。
  • 3 ) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 4 ) 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個 類 ),虛擬機器會先初始化這個主類。
  • 5 ) 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

對於這5種會觸發類進行初始化的場景,虛擬機器規範中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。下面舉3個例子來說明何為被動引用,分別見程式碼清單7-1〜程式碼清單7-3。

程式碼清單7 - 1 被動引用的例子之一

package org.fenixsoft.classloading;

/**
 * 被動使用類欄位演示一:
 * 通過子類引用父類的靜態欄位,不會導致子類初始化
 **/
public class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }
}

/**
 * 非主動使用類欄位演示
 **/
public class NotInitialization {

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

}

上述程式碼執行之後,只會輸出“Superclass init ! ” ,而不會輸出“Subclass init ! ”。對於靜態欄位,只有直接定義這個欄位的類才會被初始化 ,因此通過其子類來引用父類中定義的靜態欄位 ,只會觸發父類的初始化而不會觸發子類的初始化。至於是否要觸發子類的載入和驗證,在虛擬機器規範中並未明確規定,這點取決於虛擬機器的具體實現。對於Sun HotSpot虛擬機器來沒,可通過-XX : +TraceClassLoading引數觀察到此操作會導致子類的載入。

程式碼清單7 - 2 被動引用的例子之二

package org.fenixsoft.classloading;

/**
 * 被動使用類欄位演示二:
 * 通過陣列定義來引用類,不會觸發此類的初始化
 **/
public class NotInitialization {

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

}

為了節省版面,這段程式碼複用了程式碼清單7-1中的Superclass,執行之後發現沒有輸出“Superclass init ! ” ,說明並沒有觸發類org.fenixsoft.classloading.SuperClass的初始化階段。但是這段程式碼裡面觸發了另外一個名為“[Lorg.fenixsoftclassloading.Superclass”的類的初始化階 段 ,對於使用者程式碼來說,這並不是一個合法的類名稱,它是一個由虛擬機器自動生成的、直接繼承於java.lang.Object的子類,建立動作由位元組碼指令newarray觸發。

這個類代表了一個元素型別為org.fenixsoft.classloading.SuperClass的一維陣列,陣列中應有的屬性和方法(使用者可直接使用的只有被修飾為public的length屬性和clone()方法)都實現在這個類裡。Java語言中對陣列的訪問比C/C++相對安全是因為這個類封裝了陣列元素的訪問方法 ,而C/C++直接翻譯為對陣列指標的移動。在Java語言中,當檢查到發生陣列越界 時會丟擲java.lang.ArrayIndexOutOfBoundsException 異常。

程式碼清單7 - 3 被動引用的例子之三

package org.fenixsoft.classloading;

/**
 * 被動使用類欄位演示三:
 * 常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
 **/
public class ConstClass {

    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

/**
 * 非主動使用類欄位演示
 **/
public class NotInitialization {

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

上述程式碼執行之後,也沒有輸出”ConstClass init!”,這是因為雖然在Java原始碼中引用了ConstClass類中的常量HELLOWORLD,但其實在編譯階段通過常量傳播優化,已經將此常量的值“hello world”儲存到了Notlnitialization類的常量池中,以後Notlnitialization對常量ConstClass.HELLOWORLD的引用實際都被轉化為Notlnitializationl對自身常量池的引用了。 也就是說,實際上Notlnitialization的Class檔案之中並沒有ConstClass類的符號引用入口,這兩個類在編譯成Class之後就不存在任何聯絡了。

介面的載入過程與類載入過程稍有一些不同,針對介面需要做一些特殊說明:介面也有初始化過程,這點與類是一致的,上面的程式碼都是用靜態語句塊“static{}”來輸出初始化資訊的,而介面中不能使用“static{}”語句塊,但編譯器仍然會為介面生成“ <clinit>()”類構造器,用於初始化介面中所定義的成員變數。介面與類真正有所區別的是前面講述的5種“有且僅有”需要開始初始化場景中的第3種 :當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,並不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如引用介面中定義的常量)才會初始化。

準確地說,越界檢查不是封裝在陣列元素訪問的類中,而是封裝在陣列訪問的xaload、 xastore位元組碼指令中。

相關文章