天天看點

JVM系列十二(類加載機制).

一、類加載機制

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

類的整個生命周期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備和解析階段三個部分統稱為連接配接(Linking)。

JVM系列十二(類加載機制).

類的生命周期中,加載、驗證、準備、初始化和解除安裝這五個階段的順序是固定的,解析階段在某些情況下可以在初始化階段後再開始。

虛拟機規範中嚴格規定了有且隻有四種情況開始類的初始化階段(而加載、驗證、準備階段自然需要在此之前開始):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時;
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候;
  • 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化;對于接口則沒有這個要求,隻有在真正使用到父類接口的時候才會初始化。
  • 當虛拟機啟動的時候,虛拟機會優先初始化要執行的主類。

二、類加載過程

1. 加載

加載階段是開發期可控性最強的階段,因為加載階段不僅可以使用系統提供的類加載器來完成,也可以使用使用者自定義的類加載器來完成。

虛拟機可以從多個路徑來完成加載過程,比如 ZIP 包(jar、war等)、Applet、java.lang.reflect.Proxy、檔案(JSP 等)...

在加載階段,虛拟機需要完成以下三件事情:

  • 通過一個類的全限定名來擷取定義此類的二進制位元組流。
  • 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
  • 在 Java 堆中生成一個代表這個類的 java.lang.Class 對象,作為方法區這些資料的通路入口。

2. 驗證

驗證階段的目的是為了確定 Class 檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。比如驗證是否符合 Class 檔案格式的規範、驗證代碼語義是否符合 Java 語言的規範等。

3. 準備

準備階段是正式為類變量(被 static 修飾的變量)配置設定記憶體并設定類變量初始值(資料類型的零值)的階段,這些記憶體都将在方法區中進行配置設定。注意這裡不包括執行個體變量,執行個體變量将會在對象執行個體化的時随着對象一起配置設定在 Java 堆中。

4. 解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法和接口方法四類符号引用進行。

符号引用以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。比如 Class 檔案中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等類型的常量。

直接引用可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。如果有了直接引用,那引用的目标必定已經在記憶體中存在。

5. 初始化

初始化階段才真正開始執行類中定義的 Java 代碼,該階段根據程式制定的主觀計劃去初始化類變量和其他資源。

初始化階段就是執行 <clinit>() 方法的過程,<clinit>() 方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊(static{} 塊)中的語句合并産生的。

  • <clinit>() 不需要顯示的調用父類的 <clinit>() 方法,虛拟機會保證在子類的 <clinit>() 方法執行之前,父類的 <clinit>() 方法已經執行完畢,是以虛拟機第一個執行 <clinit>() 方法的類一定是 java.lang.Object。
  • <clinit>() 方法對于類或接口來說并不是必須的,如果沒有類變量的指派動作和靜态語句塊(static{} 塊),則不會生成 <clinit>() 方法。
  • 接口的 <clinit>() 方法不需要先執行父接口的 <clinit>() 方法,隻有當父接口定義的變量被使用時,父接口才會被初始化。
  • 虛拟機會保證一個類的 <clinit>() 方法在多線程環境中被正确地加鎖和同步。

三、類加載器

類加載器通過一個類的全限定名來擷取描述此類的二進制位元組流。

類加載器在類層次劃分、OSGi、熱部署、代碼加密等領域發揮着重要的作用。

比較兩個類是否“相等”,隻有在這兩個類是由同一個類加載器加載的前提之下才有意義,否則,即使這兩個類是來源于同一個 Class 檔案,隻要加載它們的類加載器不同,那這兩個類就必定不相等。這裡的“相等”包括 equal() 方法、isAssignableForm() 方法、isInstance() 方法和 instanceof 關鍵字。

下面的例子可以看到,雖然都是來自同一個 Class 檔案,但是因為類加載器不同,依然是兩個獨立的類,自然不會“相等”。

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    // 自定義簡單類加載器
    ClassLoader myClassLoader = new ClassLoader() {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            try {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream == null) {
                    return super.loadClass(name);
                }
                byte[] bytes = new byte[inputStream.available()];
                inputStream.read(bytes);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return super.loadClass(name);
        }
    };
    Object newInstance = myClassLoader.loadClass("org.jvm.demo.chapter7.ClassLoaderTest").newInstance();
    System.out.println(newInstance.getClass()); // org.jvm.demo.chapter7.ClassLoaderTest
    System.out.println(newInstance instanceof org.jvm.demo.chapter7.ClassLoaderTest); // false
}
           

絕大部分 Java 程式都會使用到以下三種系統提供的類加載器:

  • 啟動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 或着 -Xbootclasspath 參數指定目錄下的類庫,加載内容按檔案名識别,如 rt.jar,啟動類加載器無法被 Java 程式直接引用。
  • 擴充類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 或者 java.ext.dirs 系統變量所指定的所有類庫,該加載器由 sun.misc.Launcher$ExtClassLoader 實作,開發者可以直接使用擴充類加載器 — Launcher.getLauncher().getClassLoader()。
  • 應用程式類加載器(Application ClassLoader):負責加載使用者類路徑 ClassPath 上所指定的類庫,如果應用程式沒有自定義過自己的類加載器,一般情況下就是程式的預設類加載器,該加載器由 sun.misc.Launcher$AppClassLoader 實作,開發者可以直接使用這個類加載器 — ClassLoader.getSystemClassLoader()。

四、雙親委派模型

如圖所示類加載器之間的層次關系,就稱為類加載器的雙親委派模型。雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裡類加載器之間的父子關系一般不會以繼承的關系來實作,而是都使用組合關系來複用父加載器的代碼。

JVM系列十二(類加載機制).

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此。

雙親委派模型對于保證 Java 程式的穩定運作很重要,它讓 Java 類随着它的類加載器一起具備了一種帶有優先級的層次關系。

雙親委派模型不是一個強制性的限制模型,而是 Java 設計者們推薦給開發者們的一種類加載器的實作方式。

JDK9 後,擴充類加載器(Extension Class Loader)被平台類加載器(Platform Class Loader)取代。這其實是一個很順理成章的變動,既然整個 JDK 都基于子產品化進行建構(原來的 rt.jar 和 tools.jar 被拆分成數十個 JMOD 檔案),其中的 Java 類庫就已天然地滿足了可擴充的需求,那當然無須再保留<JAVA_HOME>\lib\ext目錄,此前使用這個目錄或者 java.ext.dirs 系統變量來擴充 JDK 功能的機制已經沒有繼續存在的價值了,用來加載這部分類庫的擴充類加載器也完成了它的曆史使命。

JDK9 後,平台類加載器和應用程式類加載器都不再派生自 java.net.URLClassLoader。啟動類加載器、平台類加載器和應用程式類加載器全部繼承于 jdk.internal.loader.BuiltinClassLoader,在 BuiltinClassLoader 中實作了新的子產品化架構下類如何從子產品中加載的邏輯,以及子產品中資源可通路性的處理。

JDK9 後,雖然仍然維持着三層類加載器和雙親委派的架構,但類加載的委派關系也發生了變動。當平台及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統子產品中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個子產品的加載器完成加載,也許這可以算是對雙親委派模型的破壞。

JVM系列十二(類加載機制).