天天看點

位元組碼與類的加載——(四)再談類的加載器

文章目錄

    • 4_再談類的加載器
      • 4.1_概述
        • 4.1.1_類加載的分類
        • 4.1.2_類加載器的必要性
        • 4.1.3_命名空間
        • 4.1.4_類加載價值的基本特征
      • 4.2_類的加載器分類
        • 4.2.1_引導類加載器
        • 4.2.2_擴充類加載器
        • 4.2.3_系統類加載器
        • 4.2.4_使用者自定義類加載器
        • 4.2.5_JDK 1.9 後的類加載器
      • 4.3_測試不同的類加載器
      • 4.4_ClassLoader源碼解析
        • 4.4.1_ClassLoader 的主要方法
        • 4.4.2_SecureClassLoader 與 URLClassLoader(BuiltinClassLoader)
        • 4.4.3_BootClassLoader
        • 4.4.4_ExtClassLoader(PlatformClassLoader) 與 AppClassLoader
        • 4.4.5_Class.forName() 與 ClassLoader.loadClass()
      • 4.5_雙親委派模型
        • 4.5.1_JDK 9 的雙親委派模式 ☆
        • 4.5.2_定義與本質
        • 4.5.3_優勢與劣勢
        • 4.5.4_破壞雙親委派機制
          • 破壞雙親委派機制 1
          • 破壞雙親委派機制 2
          • 破壞雙親委派機制 3
        • 4.5.5_熱替換的實作
      • 4.6_沙箱安全機制
      • 4.7_自定義類的加載器
        • 實作方式
      • 4.8_JDK 9 新特性

4_再談類的加載器

(JDK 1.9 後類加載器有變動,下文主要以 JDK 1.8 為主,伴随一些 JDK 1.9 變動後的新特性)

4.1_概述

類加載器是 JVM 執行類加載機制的前提。

ClassLoader 的作用:

ClassLoader 是 Java 的核心元件,所有的 Class 都是由 ClassLoader 進行加載的。

ClassLoader 負責通過各種方式将 Class 資訊的二進制資料流讀入 JVM 内部,轉換為一個與目标類對應的

java.lang.Class

對象執行個體。然後交給 Java 虛拟機進行連結、初始化等操作。

是以,ClassLoader 在整個裝載階段,隻能影響到類的加載,而無法通過 ClassLoader 去改變類的連結和初始化行為。至于它是否可以運作,則由 Execution Engine 決定。

位元組碼與類的加載——(四)再談類的加載器

類加載器最早出現在 Java 1.0 版本中,那個時候隻是單純地為了滿足 Java Applet 應用而被研發出來,但如今類加載器卻在 OSGI、位元組碼加解密領域大放異彩。這主要歸功于 Java 虛拟機的設計者們當初在設計類加載器的時候,并沒有考慮将它綁定在 JVM 内部,這樣做的好處就是能夠更加靈活和動态地執行類加載操作。

4.1.1_類加載的分類

類的加載分類:顯式加載 vs 隐式加載

Class 檔案的顯式加載與隐式加載的方式是指 JVM 加載 Class 檔案到記憶體的方式。

  • 顯式加載指的是在代碼中通過調用 ClassLoader 加載 Class 對象,如直接使用

    Class.forName(name)

    this.getClass().getClassLoader().loadClass()

    加載 Class 對象。
  • 隐式加載則是不直接在代碼中調用 ClassLoader 的方法加載 Class 對象,而是通過虛拟機自動加載到記憶體中,如在加載某個類的 Class 檔案時,該類的 Class 檔案中引用了另外一個類的對象,此時額外引用的類将通過 JVM 自動加載到記憶體中。

在日常開發中以上兩種方式一般會混合使用。

4.1.2_類加載器的必要性

一般情況下,Java 開發人員并不需要在程式中顯式地使用類加載器,但是了解類加載器的加載機制卻顯得至關重要。從以下幾個方面說:

  • 避免在開發中遇到

    java.lang.ClassNotFoundException

    異常或

    java.lang.NoClassDeFoundError

    異常時手足無措。隻有了解類加載器的加載機制才能夠在出現異常的時候快速地根據錯誤異常日志定位問題和解決問題。
  • 需要支援類的動态加載或需要對編譯後的位元組碼檔案進行加解密操作時,就需要與類加載器打交道了。
  • 開發人員可以在程式中編寫自定義類加載器來重新定義類的加載規則,以便實作一些自定義的處理邏輯。

4.1.3_命名空間

何為類的唯一性?

對于任意一個類,都需要由加載它的類加載器和這個類本身一同确認其在 Java 虛拟機中的唯一性。每一個類加載器,都擁有一個獨立的類名稱空間:**比較兩個類是否相等,隻有在這兩個類是由同一個類加載器加載的前提下才有意義。**否則,即使這兩個類源自同一個 Class 檔案,被同一個虛拟機加載,隻要加載他們的類加載器不同,那這兩個類就必定不相等。

命名空間
  • 每個類加載器都有自己的命名空間,命名空間由該加載器所有的父加載器所加載的類組成。
  • 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
  • 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。

在大型應用中,我們往往借助這一特性,來運作同一個類的不同版本。

4.1.4_類加載價值的基本特征

通常類加載機制有三個基本特征:

  • 雙親委派模型

    但不是所有類加載都遵守這個模型,有的時候,啟動類加載器所加載的類型,是可能要加載使用者代碼的,比如 JDK 内部的

    ServiceProvider/ServiceLoader

    機制,使用者可以在标準 API 架構上,提供自己的實作,JDK 也需要提供些預設的參考實作。例如,Java 中 JNDI、JDBC、檔案系統、Cipher 等很多方面,都是利用的這種機制,這種情況就不會用雙親委派模型去加載,而是利用所謂的上下文加載器。
  • 可見性

    子類加載器可以通路父加載器加載的類型,但是反過來是不允許的。不然,因為缺少必要的隔離,我們就沒有辦法利用類加載器去實作容器的邏輯。

  • 單一性

    由于父加載器的類型對于子加載器是可見的,是以父加載器中加載過的類型,就不會在子加載器中重複加載。但是注意,類加載器"鄰居"間,同一類型仍然可以被加載多次,因為互相并不可見。

4.2_類的加載器分類

JVM 支援兩種類型的類加載器,分别為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)

從概念上來講,自定義類加載器一般指的是程式中由開發人員自定義的一類類加載器,但是 Java 虛拟機規範卻沒有這麼定義,而是将所有派生于抽象類 ClassLoader 的類加載器都劃分為自定義類加載器。無論類加載器的類型如何劃分,在程式中我們最常見的類加載器結構主要是如下情況:

位元組碼與類的加載——(四)再談類的加載器
  • 除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的"父類"加載器。
  • 不同類加載器看似是繼承(Inheritance)關系,實際上是包含關系。在下層加載器中,包含着上層加載器的引用。

    如:

    class ClassLoader {
      ClassLoader parent; //父類加載器
    
      public ClassLoader(ClassLoader parent) {
        this.parent = parent;
      }
    }
    
    class ParentClassLoader extends ClassLoader {
      public ParentClassLoader(ClassLoader parent) {
        super(parent);
      }
    }
    
    class ChildClassLoader extends ClassLoader {
      public ChildClassLoader(ClassLoader parent) {
        //parent = new ParentClassLoader();
        super(parent);
      }
    }
               
JDK 9 後類加載器原理

當平台及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統子產品中,如果可以找到這樣的歸屬關系,就要優先委派給負責哪個子產品的加載器完成加載。

位元組碼與類的加載——(四)再談類的加載器

4.2.1_引導類加載器

啟動類加載器(引導類加載器)
  • 使用 C/C++ 語言實作的,嵌套在 JVM 内部。
  • 加載 Java 的核心庫(

    JAVA_HOME/jre/lib/rt.jar

    sun.boot.class.path

    路徑下的内容)。用于提供 JVM 自身需要的類。
  • 并不繼承自

    java.lang.ClassLoader

    ,沒有父加載器。
  • 出于安全考慮,Bootstrap 啟動類加載器之加載包名為

    java

    javax

    sun

    等開頭的類。
  • 加載擴充類和應用程式類加載器,并指定為他們的父類加載器。

使用

-XX:+TraceClassLoading

參數得到類加載器資訊

啟動類加載器使用 C++ 編寫的?Yes!

  • C/C++:指針函數 & 函數指針、C++ 支援多繼承、更加高效
  • Java :由 C++ 演變而來,(C++)-- 版,單繼承
啟動類加載器(Bootstrap ClassLoader)

JDK 9 之後有了

BootClassLoader

的存在,啟動類加載器現在是在 Java 虛拟機内部和 Java 類庫共同協作實作的類加載器,主要用于加載

java.base

中的核心系統類。

盡管有了

BootClassLoader

這樣的 Java 類,但為了與之前的代碼保持相容,所有在擷取啟動類加載器的場景(譬如

Object.class.getClassLoader()

)中仍然會傳回 null 來代替,而不會得到

BootClassLoader

的執行個體。

4.2.2_擴充類加載器

平台類加載器(Platform ClassLoader) jdk 9
  • JDK 9 仍然保留了三層類加載器結構,不過為了支援子產品系統,對它們做了一些調整。擴充機制被移除,擴充類加載器由于向後相容性的原因被保留,被重命名為平台類加載器(Platform ClassLoader)。可以通過

    ClassLoader

    的新方法

    getPlatformClassLoader()

    來擷取
  • 加載

    lib/modules

    目錄下的 class。確定所有 Java SE 平台上的類通過平台類加載器都是可見的
  • 用于加載 JDK 中非核心子產品
  • Java SE 平台中的某些類是由平台類加載器定義的,而另一些是由引導類加載器定義的。應用程式不應依賴于哪個類加載器定義哪個平台類
位元組碼與類的加載——(四)再談類的加載器
擴充類加載器(Extension ClassLoader)
  • Java 語言編寫,由

    sun.misc.Launcher$ExtClassLoader

    實作
  • 繼承于 ClassLoader 類
  • 父類加載器為啟動類加載器
  • java.ext.dirs

    系統屬性所指定的目錄中加載類庫,或從 JDK 的安裝目錄的

    jre/lib/ext

    子目錄下加載類庫。如果使用者建立的 JAR 放在此目錄下,也會自動由擴充類加載器加載

    下圖為 JDK 1.8

位元組碼與類的加載——(四)再談類的加載器

​ 下圖為 JDK 1.9 及之後

位元組碼與類的加載——(四)再談類的加載器

4.2.3_系統類加載器

應用程式類加載器(系統類加載器,AppClassLoader)

  • Java 語言編寫,由

    sun.misc.Launcher$AppClassLoader

    實作
  • 繼承于 ClassLoader 類
  • 父類加載器為擴充類加載器
  • 它負責加載環境變量 classpath 或系統屬性 java.class.path 指定路徑下的類庫
  • 應用程式中的類加載器預設是系統類加載器,用于加載一般的應用類
  • 它是使用者自定義類加載器的預設父加載器
  • 通過 ClassLoader 的

    getSystemClassLoader()

    方法可以擷取到該類加載器
補充 JDK 9
位元組碼與類的加載——(四)再談類的加載器

4.2.4_使用者自定義類加載器

  • 在 Java 的日常應用程式開發中,類的加載幾乎是由上述 3 種類加載器互相配合執行的。在必要時,我們還可以自定義類加載器,來定制類的加載方式。
  • Java 開發者可以自定義類加載器來實作類庫的動态加載,加載源可以是本地的 JAR 包,也可以是網絡上的遠端資源。
  • 通過類加載器可以實作非常絕妙的插件機制,這方面的實際應用案例不勝枚舉。例如,著名的 OSGI 元件架構,再如 Eclipse 的插件機制。類加載器為應用程式提供了一種動态增加新功能的機制,這種機制無需重新打包釋出應用程式就能實作。
  • 同時,自定義加載器能夠實作應用隔離,例如 Tomcat、Spring 等中間件群組件架構都在内部實作了自定義的加載器,并通過自定義加載器隔離不同的元件子產品。這種機制比 C/C++ 程式要好太多,想不修改 C/C++ 程式就能為其新增功能,幾乎是不可能的,僅僅一個相容性便能阻擋所有美好的設想。
  • 自定義類加載器通常需要繼承于 ClassLoader。

4.2.5_JDK 1.9 後的類加載器

JDK9 之前,JDK1.2 以來,類加載其遵循:三層類加載器架構和雙親委派模型。

子產品化系統的施行,類加載器發生了變化:

  • 擴充類加載器(Extension ClassLoader)被平台類加載器(Platform ClassLoader)取代。同時整個 JDK 都基于子產品化進行建構(其中原來的 rt.jar 和 tools.jar 被拆分成數十個 JMOD 檔案),其中的 Java 類庫已滿足了可擴充的需求,是以無須再保留

    \lib\ext

    目錄,之前使用這個目錄或者

    java.ext.dirs

    系統變量來擴充 JDK 功能的機制已經沒用存在的價值了。

新版本 JDK 中同時取消了

\jre

目錄,因為随時可以組合建構出程式運作所需的 JRE,舉例:

隻使用 java.base 子產品中的類型,組合 JRE

jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre

  • 平台類加載器和應用程式類加載器都不再派生自

    java.net.URLClassLoader

    ,如果有程式直接依賴了這種繼承關系,或者依賴了 URLClassLoader 類的特定方法,那代碼很可能會在 JDK 9 及更高版本的 JDK 中崩潰。現在啟動類加載器、平台類加載器、應用程式類加載器全都繼承于

    jdk.internal.loader.BuiltinClassLoader

    ,在 BuiltinClassLoader 中實作了新的子產品化架構下類如何從子產品中加載的邏輯,以及子產品中資源可通路性的處理。

檢視代碼,可以看出三者之間的包含關系:

private static class BootClassLoader extends BuiltinClassLoader {
        BootClassLoader(URLClassPath bcp) {
            super(null, null, bcp);
        }

        @Override
        protected Class<?> loadClassOrNull(String cn) {
            return JLA.findBootstrapClassOrNull(this, cn);
        }
    };
           
// 将 BootClassLoader 作為 parent 參數傳入進去
		PlatformClassLoader(BootClassLoader parent) {
            super("platform", parent, null);
        }
           
// 将 BootClassLoader 作為 parent 參數傳入進去
		AppClassLoader(PlatformClassLoader parent, URLClassPath ucp) {
            super("app", parent, ucp);
            this.ucp = ucp;
        }
           

在 Java 9 中,類加載器有了名稱。該名稱在構造方法中指定,可以通過

getName()

方法來擷取。平台類加載器的名稱是

platform

,應用類加載器的名稱是

app

。類加載器的名稱在調試與類加載器相關的問題時會非常有用。

4.3_測試不同的類加載器

每個 Class 對象都會包含一個定義它的 ClassLoader 的一個引用。

擷取 ClassLoader 的途徑

// 擷取目前類的 ClassLoader
clazz.getClassLoader();

// 獲得目前線程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader();

// 獲得系統的 ClassLoader
ClassLoader.getSystemClassLoader();
           

說明:

站在程式的角度看,引導類加載器與另外兩種類加載器(系統類加載器和擴充類加載器)并不是同一個層次意義上的加載器,引導類加載器是使用 C++ 語言編寫而成的,而另外兩種類加載器則是使用 Java 語言編寫的。

JDK 9 之後有了

BootClassLoader

的存在,啟動類加載器現在是在 Java 虛拟機内部和 Java 類庫共同協作實作的類加載器,盡管有了

BootClassLoader

這樣的 Java 類,但為了與之前的代碼保持相容,所有在擷取啟動類加載器的場景(譬如

Object.class.getClassLoader()

)中仍然會傳回 null 來代替。

數組類的 Class 對象,不是由類加載器去建立的,而是在 Java 運作期 JVM 根據需要自動建立的。對于數組類的類加載器來說,是通過

Class.getClassLoader()

傳回的,與數組當中元素類型的類加載器是一樣的:如果數組當中的元素類型是基本資料類型,數組類是沒有類加載器的;如果是引用資料類型,則使用了引導類加載器。

String[] strArr = new String[6];
System.out.println(strArr.getClass().getClassLoader());
// 運作結果:null,使用的是引導類加載器

ClassLoaderTest[] test = new ClassLoaderTest[1];
System.out.println(test.getClass().getClassLoader());
// 運作結果:
// jdk9之前:[email protected]
// jdk9之後:[email protected]

int[] inst = new int[2];
System.out.println(inst.getClass().getClassLoader());
// 運作結果:null,基本資料類型沒有類加載器
           

4.4_ClassLoader源碼解析

ClassLoader 與現有類加載的關系:

  • jdk 9 之前:
    位元組碼與類的加載——(四)再談類的加載器
  • jdk 9 及以後:
    位元組碼與類的加載——(四)再談類的加載器

除了以上虛拟機自帶的加載器外,使用者還可以定制自己的類加載器。Java 提供了抽象類

java.lang.ClassLoader

,所有使用者自定義的類加載器都應該繼承 ClassLoader 類。

4.4.1_ClassLoader 的主要方法

抽象類 ClassLoader 的主要方法:(内部沒有抽象方法)

  • public final ClassLoader getParent()

    @CallerSensitive
        public final ClassLoader getParent() {
            if (parent == null)
                return null;
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                // Check access to the parent class loader
                // If the caller's class loader is same as this class loader,
                // permission check is performed.
                checkClassLoaderPermission(parent, Reflection.getCallerClass());
            }
            return parent;
        }
               
    傳回該類加載器的超類加載器
  • public Class<?> loadClass(String name) throws ClassNotFoundException

    public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
    	protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            // 同步操作,確定隻加載一次
            synchronized (getClassLoadingLock(name)) { 
                // 檢視該類是否已經被加載
                Class<?> c = findLoadedClass(name);
                // 未被加載
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        // 展現雙親委派機制
                        // 擷取目前類加載器的父類加載器
                        if (parent != null) {
                            // 父類加載器不為空,則調用用父類加載器進行類的加載
                            c = parent.loadClass(name, false);
                        } else {
                            // 父類加載器為空,則代表父類是引導類加載器
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // 如果依舊加載不到資訊,則通過自定義實作的 findClass() 去查找并加載
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        PerfCounter.getFindClasses().increment();
                    }
                }
                // 解析
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    
    	protected final Class<?> findLoadedClass(String name) {
            if (!checkName(name))
                return null;
            return findLoadedClass0(name);
        }
               
    加載名稱為 name 的類,傳回結果為

    java.lang.Class

    類的執行個體。如果找不到類,則傳回 ClassNotFountException 異常。該方法中的邏輯就是雙親委派模式的實作。
  • protected Class<?> findClass(String name) throws ClassNotFoundException

    protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
        }
               
    查找二進制名稱為 name 的類,傳回結果為

    java.lang.Class

    類的執行個體。這是一個受保護的方法,JVM 鼓勵我們重寫此方法,需要自定義加載器遵循雙親委派機制,該方法會在檢查完父類加載器之後被

    loadClass()

    方法調用。

    在 JDK 1.2 之前,在自定義類加載時,總會去繼承 ClassLoader 類并重寫

    loadClass()

    方法,進而實作自定義的類加載類。但是在 JDK 1.2 之後已不再建議使用者去覆寫

    loadClass()

    方法,而是建議把自定義的類加載邏輯寫在

    findClass()

    方法中,從前面的分析可知,

    findClass()

    方法是在

    loadClass()

    方法中被調用的,當

    loadClass()

    方法中父加載器加載失敗後,則會調用自己的

    findClass()

    方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委派機制。

    需要注意的是 ClassLoader 類中并沒有實作

    findClass()

    方法的具體代碼邏輯,取而代之的是抛出 ClassNotFoundException 異常,同時應該知道的是

    findClass()

    方法通常是和

    defineClass()

    方法一起使用的。一般情況下,在自定義類加載器時,會直接覆寫 ClassLoader 的

    findClass()

    方法并編寫加載規則,取得要加載類的位元組碼後轉換成流,然後調用

    defineClass()

    方法生成類的 Class 對象。
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len)

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
            throws ClassFormatError
        {
            return defineClass(name, b, off, len, null);
        }
    	
        protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                             ProtectionDomain protectionDomain)
            throws ClassFormatError
        {
            protectionDomain = preDefineClass(name, protectionDomain);
            String source = defineClassSourceLocation(protectionDomain);
            Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
            postDefineClass(c, protectionDomain);
            return c;
        }
               
    根據給定的位元組數組 b 轉換為 Class 的執行個體,off 和 len 參數表示實際 Class 資訊在 byte 數組中的位置和長度,其中 byte 數組 b 是 ClassLoader 從外部擷取的。這是受保護的方法,隻有在自定義 ClassLoader 子類中可以使用

    defineClass()

    方法是用來将 byte 位元組流解析成 JVM 能夠識别的 Class 對象(ClassLoader 中已實作該方法邏輯),通過這個方法不僅能夠通過 Class 檔案執行個體化 Class 對象,也可以通過其它方式執行個體化 Class 對象,如通過網絡中接收一個類的位元組碼,然後轉換為 byte 位元組流建立對應的 Class 對象。

    defineClass() 方法通常與 findClass() 方法一起使用,一般情況下,在自定義類加載器時,會直接覆寫 ClassLoader 的 findClass() 方法并編寫加載規則,取得要加載類的位元組碼後轉換成流,然後調用 defineClass() 方法生成類的 Class 對象。

    簡單舉例:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
      //擷取類的位元組數組
      byte[] classData = getClassData(name);
      if (classData == null) {
        throw new ClassNotFoundException();
      } else {
        //使用 defineClass 生成 Class 對象
        return defineClass(name, classData, 0, classData.length);
      }
    }
               
  • protected final void resolveClass(Class<?> c)

    protected final void resolveClass(Class<?> c) {
            if (c == null) {
                throw new NullPointerException();
            }
        }
               
    連結指定的一個 Java 類。使用該方法可以使用類的 Class 對象建立完成的同時也被解析。前面我們說連結階段主要是對位元組碼進行驗證,為類變量配置設定記憶體并設定初始值同時将位元組碼檔案中的符号引用轉換為直接引用。
  • protected final Class<?> findLoadedClass(String name)

    protected final Class<?> findLoadedClass(String name) {
            if (!checkName(name))
                return null;
            return findLoadedClass0(name);
        }
    
        private final native Class<?> findLoadedClass0(String name);
               
    查找名稱為 name 的已經被加載過的類,傳回結果為

    java.lang.Class

    類的執行個體。這個方法是 final 方法,無法被修改。
  • private final ClassLoader parent;

    // The parent class loader for delegation
        // Note: VM hardcoded the offset of this field, thus all new fields
        // must be added *after* it.
        private final ClassLoader parent;
               
    它也是一個 ClassLoader 的執行個體,這個字段所表示的 ClassLoader 也稱為這個 ClassLoader 的雙親。在類加載的過程中,ClassLoader 可能會将某些請求交予自己的雙親處理。

4.4.2_SecureClassLoader 與 URLClassLoader(BuiltinClassLoader)

接着 SecureClassLoader 擴充了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要針對 Class 源碼的通路權限)的方法,一般我們不會直接跟這個類打交道,更多的是與它的子類 URLClassLoader 有所關聯。

前面說過,ClassLoader 是一個抽象類,很多方法是空的沒有實作,比如

findClass()

findResource()

等。而 URLClassLoader 這個實作類為這些方法提供了具體的實作。并新增了 URLClassPath 類協助取得 Class 位元組碼流等功能。在編寫自定義類加載器時,如果沒有太過于複雜的需求,可以直接繼承 URLClassLoader 類,這樣就可以避免自己去編寫

findClass()

方法及其擷取位元組碼流的方式,使自定義類加載器編寫更加簡潔。

注:

JDK 9 及之後使用了

BuiltinClassLoader

來代替

URLClassLoader

,它也繼承了

SecureClassLoader

,是

PlatformClassLoader

AppClassLoader

的父類。其核心的方法主要是 loadClassOrNull(…) 方法:

@Override
    protected Class<?> loadClass(String cn, boolean resolve)
        throws ClassNotFoundException
    {
        Class<?> c = loadClassOrNull(cn, resolve);
        if (c == null)
            throw new ClassNotFoundException(cn);
        return c;
    }

	protected Class<?> loadClassOrNull(String cn, boolean resolve) {
        // 同步
        synchronized (getClassLoadingLock(cn)) {
            // 檢視該類是否已經被加載
            Class<?> c = findLoadedClass(cn);

            if (c == null) {

                // 加載子產品資訊
                LoadedModule loadedModule = findLoadedModule(cn);
                if (loadedModule != null) {

                    // package is in a module
                    BuiltinClassLoader loader = loadedModule.loader();
                    if (loader == this) {
                        if (VM.isModuleSystemInited()) {
                            c = findClassInModuleOrNull(loadedModule, cn);
                        }
                    } else {
                        // delegate to the other loader
                        c = loader.loadClassOrNull(cn);
                    }

                } else {

                    // check parent
                    if (parent != null) {
                        // 有父類加載器,則調用父加載器的相關方法加載
                        c = parent.loadClassOrNull(cn);
                    }

                    // 如果沒加載到,則用目前加載器去加載
                    if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
                        // 該方法内會調用到 defineClass(...) 方法去加載類檔案
                        c = findClassOnClassPathOrNull(cn);
                    }
                }

            }

            if (resolve && c != null)
                resolveClass(c);

            return c;
        }
    }
           

4.4.3_BootClassLoader

BootClassLoader

ClassLoaders

的一個靜态内部類,雖然它從代碼實作上是 BuiltinClassLoader 的子類,但是從功能上說它是

PlatformClassLoader

AppClassLoader

的 parent 類。

private static class BootClassLoader extends BuiltinClassLoader {
        BootClassLoader(URLClassPath bcp) {
            super(null, null, bcp);
        }

        @Override
        protected Class<?> loadClassOrNull(String cn) {
            return JLA.findBootstrapClassOrNull(this, cn);
        }
    };
           

4.4.4_ExtClassLoader(PlatformClassLoader) 與 AppClassLoader

ExtClassLoader 并沒有重寫 loadClass() 方法,這足以說明其遵循雙親委派模式,而 AppClassLoader 重寫了 loadClass() 方法,但最終調用的還是父類 loadClass() 方法,是以依然遵循雙親委派模式。

注:

JDK 9 及之後,

PlatformClassLoader

也沒有重寫 loadClass() 方法。

private static class PlatformClassLoader extends BuiltinClassLoader {
        static {
            if (!ClassLoader.registerAsParallelCapable())
                throw new InternalError();
        }
		
        // 将 BootClassLoader 作為 parent 參數傳入進去
        PlatformClassLoader(BootClassLoader parent) {
            super("platform", parent, null);
        }

        /**
         * 加載子產品
         *
         * Shared classes are returned in ClassLoader::findLoadedClass
         * that bypass the defineClass call.
         */
        private Package definePackage(String pn, Module module) {
            return JLA.definePackage(this, pn, module);
        }
    }
           

而 AppClassLoader 重寫了,可以看到,最後傳回調用的還是父類 BuiltinClassLoader 的 loadClass() 方法(

loadClassOrNull(...)

)。

@Override
        protected Class<?> loadClass(String cn, boolean resolve)
            throws ClassNotFoundException
        {
            // for compatibility reasons, say where restricted package list has
            // been updated to list API packages in the unnamed module.
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                int i = cn.lastIndexOf('.');
                if (i != -1) {
                    sm.checkPackageAccess(cn.substring(0, i));
                }
            }

            return super.loadClass(cn, resolve);
        }
           

4.4.5_Class.forName() 與 ClassLoader.loadClass()

  • Class.forName()

    是一個靜态方法,最常用的是

    Class.forName(String className);

    根據傳入的類的權限定名傳回一個 Class 對象。**該方法在将 Class 檔案加載到記憶體的同時,會執行類的初始化。**如:

    Class.forName("com.orcas.java.HelloWorld");

  • ClassLoader.loadClass()

    這是一個執行個體方法,需要一個 ClassLoader 對象來調用該方法。

    **該方法将 Class 檔案加載到記憶體時,并不會執行類的初始化,直到這個類第一次使用時才進行初始化。**該方法因為需要得到一個 ClassLoader 對象,是以可以根據需要指定使用哪個類加載器,如:

    ClassLoader c1 = .....;

    c1.loadClass("com.orcas.java.HelloWorld");

4.5_雙親委派模型

任意一個 ClassLoader 在嘗試加載一個類的時候,都會先嘗試調用其父類的相關方法去加載類,如果其父類不能加載該類,則交由子類去完成。

這樣的好處:對于任意使用者自定義的 ClassLoader,都會先去嘗試讓 JVM 的 Bootstrap ClassLoader 去嘗試加載(自定義的 ClassLoader 都繼承了它們)。那麼就能保證 JVM 的類會被優先加載,限制了使用者對 JVM 系統的影響。

4.5.1_JDK 9 的雙親委派模式 ☆

JDK 9 類加載的委派關系也發生了變動:

當平台及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統子產品中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個子產品的加載器完成加載。

位元組碼與類的加載——(四)再談類的加載器

(下文是基于 JDK 8)

4.5.2_定義與本質

類加載器用來把類加載到 Java 虛拟機中。從 JDK 1.2 版本開始,類的加載過程采用雙親委派機制,這種機制能更好地保證 Java 平台的安全。

  • 定義

    一個類加載器在接到加載類的請求時,它首先不會自己嘗試去加載這個類,而是把這個請求任務委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務,就成功傳回。隻有父類加載器無法完成此加載任務時,才自己去加載。

  • 本質

    規定了類加載的順序是:引導類加載器先加載,若加載不到,由擴充類加載器加載,若還加載不到,才會由系統類加載器或自定義的類加載器進行加載。

    位元組碼與類的加載——(四)再談類的加載器

4.5.3_優勢與劣勢

雙親委派機制優勢
  • 避免類的重複加載,確定一個類的全局唯一性

Java 類随着它的類加載器一起具備了一種帶有優先級的層級關系,通過這種層級關系可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子 ClassLoader 再加載一次。

  • 保護程式安全,防止核心 API 被随意篡改

代碼支援

雙親委派機制在

java.lang.ClassLoader.loadClass(String, boolean)

接口中展現。該接口的邏輯如下:

  • ① 先在目前加載器的緩存中查找有無目标類,如果有,直接傳回
  • ② 判斷目前加載器的父加載器是否為空,如果不為空,則調用

    parent.loadClass(name, false)

    接口進行加載
  • ③ 反之,如果目前加載器的父類加載器為空,則調用

    findBootstrapClassOrNull(name)

    接口,讓引導類加載器進行加載
  • ④ 如果通過以上3條路徑都沒能成功加載,則調用

    findClass(name)

    接口進行加載。該接口最終會調用

    java.lang.ClassLoader

    接口的 defineClass 系列的 native 接口加載目标 Java 類

雙親委派的模型就隐藏在第 2 和第 3 步中

舉例

假設目前加載的是

java.lang.Object

這個類,很顯然,該類屬于 JDK 中核心的不能再核心的一個類,是以一定隻能由引導類加載器進行加載。當 JVM 準備加載

java.lang.Object

時,JVM 預設會使用系統類加載器去加載,按照上面 5 步加載的邏輯,在第 1 步從系統類的緩存中肯定查找不到該類,于是進入第 2 步。由于從系統類加載器的父類加載器是擴充類加載器,于是擴充類加載器繼續從第1步開始重複。由于擴充類加載器的緩存中也一定查找不到該類,是以進入第 2 步。擴充類的父加載器是 null,是以系統調用

findClass(String)

,最終通過引導類加載器進行加載。

思考

如果在自定義的類加載器中重寫

java.lang.ClassLoader.loadClass(String)

java.lang.ClassLoader.loadClass(String, boolean)

方法,抹去其中的雙親委派機制,僅保留上面這4步中的第1步和第4步,那麼是不是就能夠加載核心類庫了呢?

這也不行!因為 JDK 還為核心類庫提供了一層保護機制。不管是自定義的類加載器,還是系統類加載器抑或擴充類加載器,最終都必須調用

java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain)

方法,而該方法會執行 preDefineClass() 接口,該接口中提供了對 JDK 核心類庫的保護。

雙親委派模式的弊端

檢查類是否加載的委派過程是單向的,這個方式雖然從結構上說比較清晰,使各個 ClassLoader 的職責非常明确,但是同時會帶來一個問題,即頂層的 ClassLoader 無法通路底層的 ClassLoader 所加載的類。

通常情況下,啟動類加載器中的類為系統核心類,包括一些重要的系統接口,而在應用類加載器中,為應用類。按照這種模式,**應用類通路系統類自然是沒有問題,但是系統類通路應用類就會出現問題。**比如在系統類中提供了一個接口,該接口需要在應用類中得以實作,該接口還綁定一個工廠方法,用于建立該接口的執行個體,而接口和工廠方法都在啟動類加載器中。這時,就會出現該工廠方法無法建立由應用類加載器加載的應用執行個體的問題。

結論

**由于 Java 虛拟機規範并沒有明确要求類加載器的加載機制一定要使用雙親委派模型,隻是建議采用這種方式而已。**比如 Tomcat 中,類加載器所采用的加載機制就和傳統的雙親委派模型有一定差別,當預設的類加載器接收到一個類的加載任務時,首先會由它自行加載,當它加載失敗時,才會将類的加載任務委派給它的超類加載器去執行,這同時也是 Servlet 規範推薦的一種做法。

4.5.4_破壞雙親委派機制

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

在 Java 的世界中大部分的類加載器都遵循這個模型,但也有例外情況,直到 Java 子產品化出現為止,雙親委派模型主要出現過 3 次較大規模"被破壞"的情況。

破壞雙親委派機制 1

第一次破壞雙親委派機制:

雙親委派模型的第一次"被破壞"其實發生在雙親委派模型出現之前——即 JDK 1.2 面世以前的"遠古"時代。

由于雙親委派模型在 JDK 1.2 之後才被引入,但是類加載器的概念和抽象類 java.lang.ClassLoader 則在 Java 的第一個版本中就已經存在,面對已經存在的使用者自定義類加載器的代碼,Java 設計者們引入雙親委派模型時不得不做出一些妥協,為了相容這些已有的代碼,無法再以技術手段避免 loadClass() 被子類覆寫的可能性,隻能在 JDK 1.2 之後的 java.lang.ClassLoader 中添加一個新的 protected 方法 findClass(),并引導使用者編寫的類加載邏輯時盡可能去重寫這個方法,而不是在 loadClass() 中編寫代碼。

上節我們已經分析過 loadClass() 方法,雙親委派的具體邏輯就實作在這裡面,按照 loadClass() 方法的邏輯,如果父類加載失敗,會自動調用自己的 findClass() 方法來完成加載,這樣既不影響使用者按照自己的意願去加載類,又可以保證新寫出來的類加載器是符合雙親委派規則的。

破壞雙親委派機制 2

第二次破壞雙親委派機制:線程上下文類加載器

雙親委派模型的第二次"被破壞"是由這個模型自身的缺陷導緻的,雙親委派很好地解決了各個類加載器協作時基礎類型的一緻性問題(越基礎的類由越上層的加載器進行加載)。

基礎類型之是以被稱為"基礎",是因為它們總是作為被使用者代碼繼承、調用的 API 存在,但程式設計往往沒有絕對不變的完美規則,如果有基礎類型又要調用回使用者代碼,那該怎麼辦?

這并非是不可能出現的事情,一個典型的例子便是 JNDI 服務,JNDI 現在已經是 Java 的标準服務,它的代碼由啟動類加載器來完成加載(在 JDK 1.3 時加入到 rt.jar),肯定屬于 Java 中很基礎的類型了。但 JNDI 存在的目的就是對資源進行查找和集中管理,它需要調用由其它廠商實作并部署在應用程式的 ClassPath 下的 JNDI 服務提供者接口(Service Provider Interface. SPI)的代碼。

現在問題來了,啟動類加載器時絕對不可能認識、加載這些代碼的,那該怎麼辦?(SPI:在 Java 平台中,通常把核心類 rt.jar 中提供外部服務、可由應用層自行實作的接口稱為 SPI)。

為了解決這個困境,Java 的設計團隊隻好引入了一個不太優雅的設計:**線程上下文類加載器(Thread Context ClassLoader)。**這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoader() 方法進行設定,如果建立線程時還未設定,它将會從父線程中繼承一個,如果在應用程式的全局範圍内都沒有設定過的話,那這個類加載器預設就是應用程式類加載器。

有了線程上下文類加載器,程式就可以做一些"舞弊"的事情了。JNDI 服務使用這個線程上下文類加載器去加載所需的 SPI 服務代碼。這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則,但也是無可奈何的事情。Java 中涉及 SPI 的加載基本上都采用這種方式來完成,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。不過,當 SPI 的服務提供者多于一個的時候,代碼就隻能根據具體提供者的類型來寫死判斷,為了消除這種極不優雅的方式,在 JDK 6 時,JDK 提供了 java.util.ServiceLoader 類,以 META-INF/Services 中的配置資訊,輔以責任鍊模式,這才算是給 SPI 的加載提供了一種相對合理的解決方案。

位元組碼與類的加載——(四)再談類的加載器

預設上下文加載器就是應用類加載器,這樣以上下文加載器為中介,使得啟動類加載器中的代碼也可以通路應用類加載器中的類。

破壞雙親委派機制 3

第三次破壞雙親委派機制:

雙親委派模型的第三次"被破壞"是由于使用者對程式動态性的追求而導緻的。如:代碼熱替換(Hot Swap)、**子產品熱部署(Hot Deployment)**等。

IBM 公司主導的 JSR-291(即 OSGI R4.2)實作子產品化熱部署的關鍵是它自定義的類加載器機制的實作,每個程式子產品(OSGI 中稱為 Bundle)都有一個自己的類加載器,當需要更換一個 Bundle 時,就把 Bundle 連同類加載器一起換掉以實作代碼的熱替換。在 OSGI 環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加複雜的網狀結構。

當收到類加載請求時,OSGI 将按照下面的順序進行類搜尋:

  1. 将以 java.* 開頭的類,委派給父類加載器加載
  2. 否則,将委派清單名單内的類,委派給父類加載器加載
  3. 否則,将 Import 清單中的類,委派給 Export 這個類的 Bundle 的類加載器加載
  4. 否則,查找目前 Bundle 的 ClassPath,使用自己的類加載器加載
  5. 否則,查找類是否在自己的 Fragment Bundle 中,如果在,則委派給 Fragment Bundle 的類加載器加載
  6. 否則,查找 Dynamic Import 清單的 Bundle,委派給對應 Bundle 的類加載器加載
  7. 否則,類查找失敗

說明:隻有開頭兩點仍然符合雙親委派模型的原則,其餘的類查找都是在平級的類加載器中進行的。

小結:

這裡,我們使用了"被破壞"這個詞來形容上述不符合雙親委派模型原則的行為,但這裡"被破壞"并不一定是帶有貶義的。隻要有明确的目的和充分的理由,突破舊有原則無疑是一種創新。

正如:OSGI 中的類加載器的設計不符合傳統的雙親委派的類加載器架構,且業界對其為了實作熱部署而帶來的額外的高複雜度還存在不少争議,但對這方面有了解的技術人員基本還是能達成一個共識,認為 OSGI 中對類加載器的運用是值得學習的,完全弄懂了 OSGI 的實作,就算是掌握了類加載器的精髓。

4.5.5_熱替換的實作

熱替換是指在程式運作過程中,不停止服務,隻通過替換程式檔案來修改程式的行為。熱替換的關鍵需求在于服務不能中斷,修改必須立即表現正在運作的系統之中。基本上大部分腳本語言都是天生支援熱替換的,比如:PHP,隻要替換了 PHP 源檔案,這種改動就會立即生效,而無需重新開機 Web 伺服器。

但對 Java 來說,熱替換并非天生就支援,如果一個類已經加載到系統中,通過修改類檔案,并無法讓系統再來加載并重定義這個類。是以,在 Java 中實作這一功能的一個可行的方法就是靈活運用 ClassLoader。

注意:由不同 ClassLoader 加載的同名類屬于不同的類型,不能互相轉換和相容。即兩個不同的 ClassLoader 加載同一個類,在虛拟機内部,會認為這 2 個類是完全不同的。

根據這個特點,可以用來模拟熱替換的實作,基本思路如下圖所示:

位元組碼與類的加載——(四)再談類的加載器

4.6_沙箱安全機制

  • 保護程式安全
  • 保護 Java 原生的 JDK 代碼

Java 安全模型的核心就是 Java 沙箱(Sandbox),什麼是沙箱?沙箱就是一個限制程式運作的環境。

沙箱機制就是将 Java 代碼**限定在虛拟機(JVM)特定的運作範圍中,并且嚴格限制代碼對本地系統資源通路。**通過這樣的措施來保證對代碼的有限隔離,防止對本地系統造成破壞。

沙箱主要限制系統資源通路,那系統資源包括什麼?CPU、記憶體、檔案系統、網絡。不同級别的沙箱對這些資源通路的限制也可以不一樣。

所有的 Java 程式運作都可以指定沙箱,可以定制安全政策。

安全模型
位元組碼與類的加載——(四)再談類的加載器
  • JDK 1.0 時期

    在 Java 中将執 JDK 1.0 時期行程式分成本地代碼和遠端代碼兩種,本地代碼預設視為可信任的,而遠端代碼則被看作是不受信的。對于授信的本地代碼,可以通路一切本地資源。而對于非授信的遠端代碼在早期的 Java 實作中,安全依賴于沙箱(Sandbox)機制。

  • JDK 1.1 時期

    JDK 1.0 中如此嚴格的安全機制也給程式的功能擴充帶來障礙,比如當使用者希望遠端代碼通路本地系統的檔案時候,就無法實作。

    是以在後續的 JDK 1.1 版本中,針對安全機制做了改進,增加了安全政策。允許使用者指定代碼對本地資源的通路權限。

  • JDK 1.2 時期

    在 JDK 1.2 版本中,再次改進了安全機制,增加了代碼簽名。不論本地代碼或是遠端代碼,都會按照使用者的安全政策設定,由類加載器加載到虛拟機中權限不同的運作空間,來實作差異化的代碼執行權限控制。

  • JDK 1.6 時期

    目前最新的安全機制實作,則引入了域(Domain)的概念

    虛拟機會把所有代碼加載到不同的系統域和應用域。系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行通路。虛拟機中不同的受保護域(Protected Domain),對應不一樣的權限(Permission)。存在于不同域中的類檔案就具有了目前域的全部權限。

4.7_自定義類的加載器

為什麼要自定義類加載器?
  • 隔離加載類

    在某些架構内進行中間件與應用的子產品隔離,把類加載到不同的環境。比如:阿裡内某容器架構通過自定義類加載器確定應用中依賴的 jar 包不會影響到中間件運作時使用的 jar 包。再比如:Tomcat 這類 Web 應用伺服器,内部自定義了好幾種類加載器,用于隔離同一個 Web 應用伺服器上的不同應用程式。(類的仲裁 => 類沖突)

  • 修改類加載的方式

    類的加載模型并非強制,除 Bootstrap 外,其他的加載并非一定要引入,或者根據實際情況在某個時間點按需進行動态加載。

  • 擴充加載源

    比如從資料庫、網絡、甚至是電視機機頂盒進行加載。

  • 防止源碼洩露

    Java 代碼容易被編譯和篡改,可以進行編譯加密。那麼類加載也需要自定義,還原加密的位元組碼。

常見的場景
  • 實作類似程序内隔離,類加載器實際上用作不同的命名空間,以提供類似容器、子產品化的效果。例如,兩個子產品依賴于某個類庫的不同版本,如果分别被不同的容器加載,就可以互不幹擾。這個方面的集大成者是 Java EE 和 OSGI、JPMS 等架構
  • 應用需要從不同的資料源擷取類定義資訊,例如網絡資料源,而不是本地檔案系統。或者是需要自己操縱位元組碼,動态修改或者生成類型
注意

在一般情況下,使用不同的類加載器去加載不同的功能子產品,會提高應用程式的安全性。但是,如果涉及 Java 類型轉換,則加載器反而容易産生不美好的事情。在做 Java 類型轉換時,隻有兩個類型都是由同一個加載器所加載,才能進行類型轉換,否則轉換時會發生異常。

實作方式

使用者通過定制自己的類加載器,這樣可以重新定義類的加載規則,以便實作一些自定義的處理邏輯。

1、實作方式

  • Java 提供了抽象類 java.lang.ClassLoader,所有使用者自定義的類加載器都應該繼承 ClassLoader 類
  • 在自定義 ClassLoader 的子類時候,我們常見的會有兩種做法:
    • 方式一:重寫 loadClass() 方法
    • 方式二:重寫 findClass() 方法

2、對比

這兩種方法本質上差不多,畢竟 loadClass() 也會調用 findClass(),但是從邏輯上講我們最好不要直接修改 loadClass() 的内部邏輯。建議的做法是隻在 findClass() 裡重寫自定義類的加載方法,根據參數指定類的名字,傳回對應的 Class 對象的引用。

  • loadClass() 這個方法是實作雙親委派模型邏輯的地方,擅自修改這個方法會導緻模型被破壞,容易造成問題。是以我們最好是在雙親委派模型架構内進行小範圍的改動,不破壞原有的穩定結構。同時,也避免了自己重寫 loadClass() 方法的過程中必須寫雙親委托的重複代碼,從代碼的複用性來看,不直接修改這個方法始終是比較好的選擇。
  • 當編寫好自定義類加載器後,便可以在程式中調用 loadClass() 方法來實作類加載操作。

3、說明

  • 其父類加載器是系統類加載器
  • JVM 中的所有類加載都會使用

    java.lang.ClassLoader.loadClass(String)

    接口(自定義類加載器并重寫

    java.lang.ClassLoader.loadClass(String)

    接口的除外),連 JDK 的核心類庫也不能例外。

4.8_JDK 9 新特性

為了保證相容性,JDK 9 沒有從根本上改變三層類加載器架構和雙親委派模型,但為了子產品化系統的順利運作,仍然發生了一些值得被注意的變動。

  • 擴充機制被移除,擴充類加載器由于向後相容性的原因被保留,不過被重命名為平台類加載器(Platform Class Loader)。可以通過 ClassLoader 的新方法

    getPlatformClassLoader()

    來擷取。

    JDK 9 時基于子產品化進行建構(原來的 rt.jar 和 tools.jar 被拆分成數十個 JMOD 檔案),其中的 Java 類庫就已天然地滿足了可擴充的需求,那自然無需再保留

    \lib\ext

    目錄,此前使用這個目錄或者

    java.ext.dirs

    系統變量來擴充 JDK 功能的機制已經沒有繼續存在的價值了。
  • 平台類加載器和應用程式類加載器都不再繼承自

    java.net.URLClassLoader

    現在啟動類加載器、平台類加載器、應用程式類加載器全都繼承于

    jdk.internal.loader.BuiltinClassLoader

位元組碼與類的加載——(四)再談類的加載器

​ 如果有程式直接依賴了這種繼承關系,或者依賴了 URLClassLoader 類的特定方法,那代碼很可能會在 JDK 9 及更高版本的 JDK 中崩潰。

  • 在 Java 9 中,類加載器有了名稱。該名稱在構造方法中指定,可以通過 getName() 方法來擷取。平台類加載器的名稱是 Platform,應用類加載器的名稱是 App。類加載器的名稱在調試與類加載器相關的問題時會非常有用
  • 啟動類加載器現在是在 JVM 内部和 Java 類庫共同協作實作的類加載器(以前是 C++ 實作),但為了與之前代碼相容,在擷取啟動類加載器的場景中仍然會傳回 null,而不會得到 BootClassLoader 執行個體
  • 類加載的委派關系也發生了變動

    當平台及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統子產品中,如果可以找到這樣的歸屬關系,就要優先委派給負責哪個子產品的加載器完成加載。

    位元組碼與類的加載——(四)再談類的加載器
附加

在 Java 子產品化系統明确規定了三個類加載器負責各自加載的子產品:

  • 啟動類加載器負責加載的子產品
java.base                               java.security.sasl
java.datatransfer                       java.xml
java.desktop                            jdk.httpserver
java.instrument                         jdk.internal.vm.ci
java.logging                            jdk.management
java.management                         jdk.management.agent
java.management.rmi                     jdk.naming.rmi
java.naming                             jdk.net
java.prefs                              jdk.sctp
java.rmi                                jdk.unsupported
           
  • 平台類加載器負責加載的子產品
java.activation*                    jdk.accessibility
java.compiler*                      jdk.charsets
java.corba*                         jdk.crypto.cryptoki
java.scripting                      jdk.crypto.ec
java.se                             jdk.dynalink
java.se.se                          jdk.incubator.httpclient
java.security.jgss                  jdk.internal.vm.compiler*
java.smartcardio                    jdk.jsobject
java.sql                            jdk.localedata
java.sql.rowset                     jdk.naming.dns
java.transaction*                   jdk.scripting.nashorn
java.xml.bind*                      jdk.security.auth
java.xml.crypto                     jdk.security.jgss
java.xml.ws*                        jdk.xml.dom
java.xml.ws.annotation*             jdk.zipfs
           
  • 應用程式類加載器負責加載的子產品
jdk.aot                             jdk.jdeps
jdk.attach                          jdk.jdi
jdk.compiler                        jdk.jdwp.agent
jdk.editpad                         jdk.jlink
jdk.hotspot.agent                   jdk.jshell
jdk.internal.ed                     jdk.jstatd