banner
jzman

jzman

Coding、思考、自觉。
github

虛擬機類加載機制

今天介紹一下 JVM 類加載器機制,主要內容如下:

  1. 概述
  2. 類加載的時機
  3. 類加載的過程
  4. 類加載器
  5. 類加載器分類
  6. 雙親委託模型

概述#

JVM 把字節碼 (.class) 文件加載到內存中,並對數據進行校驗、解析和初始化,最終生成可以被 JVM 直接使用的 Java 類型,這就是 JVM 的類加載機制。

在 Java 中各種類型的加載、連接和初始化過程都是在程序運行期間完成的,這種方式會在類加載時帶來一些性能開銷,但是具有很高的靈活性,Java 的動態擴展的語言特性就是依賴運行期間動態加載和動態鏈接這個特點實現的,如插件化技術中通過自定義類加載器實現資源的加載和替換,其中就是用的 Java 語言運行期間類加載的特性。

類加載的時機#

類從被加載到 JVM 內存中開始,一直到從 JVM 內存中卸載位置,類加載的生命周期如下圖所示:

image

加載、驗證、準備、初始化、卸載這五個階段的順序是確定的,類的解析則不一定,可能會在初始化之後再進行,這是為了支持 Java 語言的運行時綁定,在整個類加載的過程中,每一個階段都由前一個階段觸發進行。

JVM 規範中規定了類的初始化階段,但是加載這個階段沒有進行約束,具體由 JVM 實現自己控制,當然加載、驗證、準備必須在初始化這個階段之前完成。

那麼什麼情況下類開始初始化呢,JVM 嚴格規定了下面這些情況必須對類進行初始化:

  1. 遇到 new、getstatic/putstatic、invokestatic 指令時,如果該類沒有被初始化,則需對類進行初始化,上面指令分別對應使用 new 關鍵字進行對象實例化、讀取或設置一個靜態屬性、調用靜態方法,具體可以使用 javap 命令查看字節碼文件的實現來驗證;
  2. 使用 java.lang.reflect 對類進行反射調用的時候,如果該類沒有被初始化,則需對類進行初始化;
  3. 當初始化一個類的時候,如果其父類還沒有進行初始化,則先進行該類父類的初始化;
  4. 當 JVM 啟動時,用戶指定要啟動的主類,比如還有 main 方法的類,JVM 會先初始化這個類;
  5. 當使用 JDK 1.7 的動態語言支持時,如果 java.lang.invoke.MethodHandle 實例最後解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic,如果這些句柄對應的類沒有進行初始化,則需先對其進行初始化,至於 MethodHandle 可以理解為反射的另一種形式。

類加載的過程#

下面針對類加載的幾個階段進行具體說明。

加載#

class 文件通過類加載器將其中的字節碼內容加載到內存中,將這個靜態數據轉換成方法區中的運行時數據結構,在堆內存中生成該 class 文件對應的 java.lang.Class 對象,這個 Class 對象就是訪問方法區類數據的入口。

JVM 規範並沒有規定 class 文件的來源,舉例如下:

  • 從 zip 包中獲取,最終成為 jar、war 格式的基礎。
  • 從網絡中獲取,典型應用就是 Applet。
  • 運行時生成,典型應用就是動態代理技術,在 java.lang.reflect.Proxy 中就是使用 ProxyGenerator.generatrProxyClass 來為特定接口生成形如 Proxy 的代理類的二進制字節流。
  • 其他文件生成、數據庫中獲取等。

類的加載階段與後面的鏈接階段的過程時交叉進行的,沒有明確的界限,加載階段尚未完成,鏈接階段可能已經開始,但是兩個階段的開始時間還是保持著固定的先後順序。

鏈接#

鏈接包括驗證、準備、解析三個階段,

  • 驗證:確保 class 文件的字節流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機自身的安全,從整體來看,驗證階段主要包括文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證,具體驗證內容可以自行查看 Java 虛擬機規範。
  • 準備:正式為類變量分配內存並設置類變量的初始值,這個初始值一般是數據類型的初始值,而不是真正代碼中初始化的值,如 int 初始值就是 0,這些類變量使用的內存都將在方法區進行分配,類變量指的就是被 static 關鍵字修飾過的變量。
  • 解析:JVM 將常量池中的符號引用替換為直接引用,這裡的符號引用就是在前面驗證階段提到的符號引用驗證中的符號引用。

初始化#

類初始化階段是類加載階段的最後一步,前面的加載階段、鏈接階段除了用戶自定義類加載其參與外,其餘操作都是由 JVM 來完成的,初始化階段才真正開始執行 Java 代碼,也就是字節碼,關於類的初始化可以了解一下幾點:

  1. 初始化階段就是執行類構造器 () 方法的過程。
  2. () 方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊 static {} 中的語句合併產生的,編譯器收集順序和源碼中語句順序一致,如靜態語句塊中只能訪問定義在它之前的變量,定義在它後面的變量只能複製不能訪問。
  3. 初始化一個類時,如果父類還沒初始化,則先進行父類的初始化。
  4. JVM 會保證一個類的 () 方法在多線程環境中被正確的加鎖、同步。
  5. 當訪問一個 Java 類的靜態域時,只有真正聲明這個類才會被初始化。

類加載器#

顧名思義,類加載器 (class loader) 用來加載 Java 類到 JVM 中的,所有的類加載器都是 java.lang.ClassLoader 類的一個實例,前面知道在類的加載階段會通過類加載器來加載 class 文件,也就是可以通過一個類的全限定名來獲取定義此類的二進制字節流,這個動作的代碼實現就是類加載器的實現。

對於任意一個類,都需要加載它的類加載器和這個類本身一同確立其在 JVM 中的唯一性,每個類加載器都擁有獨立的類名稱空間,也就是說,兩個相同的類被不同的類加載器加載後將不再相等。

類加載器分類#

從 JVM 的角度來說,只存在兩種不同的類加載器:

  1. 啟動類加載器(Bootstrap ClassLoader):一般使用 C++ 語言實現,具體由 JVM 實現。
  2. 其他類加載器:使用 Java 語言實現,獨立於 JVM 之外,且都是 java.lang.ClassLoader 的一個實例,如 Android 中的 DexClassLoader。

從 Java 開發人員的角度來說,類加載器可以分為三類:

  1. 啟動類加載器(Bootstrap ClassLoader):負責加載的是 JAVA_HOME\lib 下的類庫,或者被 -Xbootclasspath 參數所指定的路徑中的並且是 JVM 識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載),啟動類加載器無法被 JAVA 程序直接應用。

  2. 擴展類加載器(Extension ClassLoader):這個類加載器由 sun.misc.Launcher$ExtClassLoader 實現,負責加載 JAVA_HOME\lib\ext 下的類,或者是被 java.ext.dirs 系統變量所指定的路徑下的類庫,可以直接使用擴展類加載器。

  3. 應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 實現,這個類加載器是 ClassLoader 中的 getSystemClassLoader () 方法的返回值,一般也稱它為系統類加載器,負責加載用戶類路徑(ClassPath)下所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,這個就是程序中默認的類加載器。

雙親委託模型#

先來看一下上面類加載器的關係:

image

上圖中展示的類加載器之間的這種層次關係,稱為類加載器的雙親委派模型( Parents Delegation Model),雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器,這裡類加載器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來復用父加載器的代碼,這種方式並不是一個強制性的約束模型,而是 Java 設計者推薦給開發者的一種類加載器實現方式。

那麼雙親委派模型的工作流程是什麼呢?

當一個類加載器收到類加載的請求,首先不會去加載這個類,而是把這個類加載的請求委託給父類類加載器,以此類推,最終每個類加載的請求都會委託給啟動類加載器,只有當父類類加載器無法完成該類的加載,子類加載器才會嘗試自己去加載,加載過程參考如下:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // 1. 檢查是否已經加載過類
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 如果沒有被加載,則調用父類的類加載器加載
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果父類的類加載器不存在,則直接使用啟動類加載器進行加載
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // 4. 父類或啟動類加載器都沒有加載該類,則調用自己的也就是子類類加載器的 findClass 方法進行類加載
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

JDK 1.2 之後的 java.lang.ClassLoader 添加了一個新的 protected 方法 findClass (),如果要自定義類加載器,則直接實現 findClass () 方法即可而不必重寫 loadClass () 方法,因為 loadClass () 方法最終調用了 findClass () 方法,這樣自定義的類加載器就是符合雙親委託規則的。

前面介紹了 JVM 類加載機制以及類加載器的相關知識,類加載器很好的支持了 Java 的動態擴展特性,在 Android 中也有使用,如在插件化技術中用到的 PathClassLoader、DexClassLoader 都是 ClassLoader 的間接子類,正是這種對 class 文件來源沒有進行限制,基於此可以實現 App 的插件化。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。