深入理解Java虛擬機之類加載機制篇


概述

虛擬機把描述類的數據從 Class 文件加載到內存中,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,就是虛擬機的類加載機制

​ 在Java語言里面,類型的加載連接初始化過程都是在程序運行期間完成的,這種策略雖然增加了類加載時系統額外的開銷,但是能給 Java 應用程序提供高度的靈活性,Java 的動態擴展優勢就依賴於運行期動態加載動態連接來實現的。例如用戶可以在編寫一個面向接口的應用程序時,可以等到運行時再指定具體的實現類,還可以通過 Java 預定義的和自定義類加載器,讓一個本地的應用程序在運行時從網絡或其他地方加載一個二進制流作為程序代碼的一部分。

類加載的生命周期

​ 類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、准備(Preparation)、解析(Resoulution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證准備解析這三個部分統稱為連接(Linking),這7個階段的發生順序如下所示:

​ 圖中,雖然加載驗證准備初始化卸載這5個階段的順序是確定的,但是類的加載過程並不是一定會按照這些順序執行的,這些階段都是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段;解析階段在某些情況下可以在初始化階段進行時,再進行解析,為了能夠滿足 Java 語言的運行時綁定(動態綁定)

​ Java 虛擬機規范並沒有強制確定類加載過程的第一個階段是加載,這主要取決於虛擬機的具體實現。但對於初始化階段,虛擬機規范明確規定了面臨這以下 5 種情況時,必須對類進行“初始化”(而加載、驗證、准備自然需要在此之前開始):

  1. 當遇到newgetstaticputstaticinvokestatic 這 4 條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。這 4 條字節碼指令的應用場景主要有:使用 new 關鍵字實例化對象的時候讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法時
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個類。
  5. 當使用 JDK1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結果 REF_getStaticREF_putStaticREF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

對於這 5 種能觸發初始化的特殊場景,這些場景的行為統稱為對一個類進行主動引用,除此之外,所有引用類的方法都不會觸發初始化,稱為被動引用。並且,對於上述第3點:當一個類在初始化時,要求其父類全部都已經初始化過了;但是一個接口在初始化時,並不強制要求其全部父接口都完成了初始化,只有等到真正使用到具體的父接口的時候(如引用接口中定義的靜態常量等)才會進行觸發對應的父接口初始化。

類加載器

概述

​ 虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取定義此類的二進制字節流”這個動作方法放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊就被稱為“類加載器”

​ 類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不止於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器都擁有一個獨立的類名稱空間。或者說,比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則的話,即使這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,但只要加載它們的類加載器不同,那這兩個類就必定不相等。這里說的“相等”,包括代表類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括使用 instanceof 關鍵字做對象所屬關系判定等情況。

雙親委派模型

​ 從 Java 虛擬機角度來說,類加載器主要分兩種:一種為啟動類加載器(Bootstrap ClassLoader),是使用C++語言進行編寫的,屬於虛擬機中的一部分;另一種就是所有其他類加載器,這些類加載器都由 Java 語言實現,是獨立於虛擬機外部的,都繼承自抽象類 java.lang.ClassLoader。接下來講講,Java 中用的最多的三種類加載器。

  • 啟動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 re.jar ,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用null即可。
  • 擴展類加載器(Extension ClassLoader):這個加載器由 sun.misc.Launcher$ExtClassLoader 實現,它負責加載 <JAVA_HOME>\lib\ext 目錄中的或被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器
  • 應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以也稱為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般就會指定該類加載器為程序中默認的類加載器。

除了這3個類加載器之外,我們還可以自定義自己的類加載器。類加載器之間的關系如下圖:

​ 以上展示的類加載器之間的這種層次關系,稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有本身的父級類加載器。這里的類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來復用父加載器的代碼。

​ 雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是將這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

​ 使用雙親委派模型來組織類加載器之間的關系,Java 類隨着它的類加載器一起具備了一種帶有優先級的層級關系。例如類 Java.lang.Object,它存放在 rt.jar 之中,無論哪一個類加載器要加載這個類,最終都是要委派給處於模型最頂端的啟動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都屬於同一個類。如果沒有采用雙親委派模型,讓各個類加載器自行去加載的話,假設用戶自定義了一個 java.lang.Object 的類,並同時放在程序的 ClassPath 中,那么系統將存在多個不同實現的Object類,應用程序就會變得一片混亂。

​ 從上圖中我們也可以看到,存在着兩個自定義類加載器;實現自定義類加載器的步驟:繼承 ClassLoader,重寫 findClass() 方法,調用 defineClass() 方法。

自定義類加載器的應用場景:

  1. 隔離加載類。在某些框架內進行中間件與應用的模塊分離,把類加載到不同的環境。
  2. 修改類加載方式。類的加載模型並非強制,除Bootstrap外,其他的加載並非一定要引入,或者根據實際情況在某個時間點進行按需進行動態加載。
  3. 擴展加載源。比如從數據庫、網絡中進行加載。
  4. 防止源碼泄露。Java 代碼容易被編譯和篡改,可以進行編譯加密。

​ 雙親委派模型在 Java 程序中發揮着很大的作用,並且實現起來相對簡單,雙親委派的實現代碼都編寫在 java.lang.ClassLoaderloadClass() 方法里面。實現邏輯是:首先會先去檢查類是否被加載過,倘若沒有加載就會調用父加載器的 loadClass() 方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載器加載失敗,就會拋出 ClassNotFoundException 異常,最后就會調用自己的 findClass() 方法進行加載。下面看下 loadClass() 源碼方法的簡要實現:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,檢查類是否已經加載
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
            	// 類沒有加載就會調用父加載器的loadClass()方法
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                	//	若父加載器為空則默認使用啟動類加載器作為父加載器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父加載器拋出 ClassNotFoundException
                // 說明父類加載器無法完成加載請求
            }
            if (c == null) {
                // 在父類加載器無法加載的時候
                // 再調用本身的findClass方法來進行
                long t1 = System.nanoTime();
                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;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM