JVM19-類加載器詳解

歡迎你們關注 github.com/hsfxuebao/j… ,但願對你們有所幫助,要是以爲能夠的話麻煩給點一下Star哈html

轉自www.cnblogs.com/xjwhaha/p/1…java

1. 概述

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

1607603872903

ClassLoader 的做用:github

ClassLoader 是 Java 的核心組件,全部的 Class 都是由 ClassLoader 進行加載的,ClassLoader 負責經過各類方式將 Class 信息的二進制數據流讀入 JVM 內部,轉換爲一個與目標類對應的 java.lang.Class 對象實例。而後交給 Java 虛擬機進行連接、初始化等操做。所以,ClassLoader 在整個裝載階段,只能影響到類的加載,而沒法經過 ClassLoader 去改變類的連接和初始化行爲。至於它是否能夠運行,則由 Execution Engine (執行引擎)決定sql

類加載:shell

類加載器最先出如今 Java 1.0 版本中,那個時候只是單純地爲了知足 Java Applet 應用而被研發出來,但現在類加載器卻在 OSGI、字節碼加解密領域大放 異彩。這主要歸功於 Java 虛擬機的設計者們當初在設計類加載器的時候,並沒 有考慮將它綁定在 JVM 內部,這樣作的好處就是可以更加靈活和動態地執行類 加載操做數據庫

類加載的分類bootstrap

類的加載分類:數組

顯式加載 vs 隱式加載緩存

Class 文件的顯式加載與隱式加載的方式是指 JVM 加載 Class 文件到內存 的方式

  • 顯式加載指的是在代碼中經過調用 ClassLoader 加載 Class 對象,如直接使 用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加載 Class 對象
  • 隱式加載則是不直接在代碼中調用 ClassLoader 的方法加載 Class 對象,而 是經過虛擬機自動加載到內存中,如在加載某個類的 Class 文件時,該類的 Class 文件中引用了另一個類的對象,此時額外引用的類將經過 JVM 自 動加載到內存中

在平常開發中以上兩種方式通常會混合使用

類加載器的必要性

通常狀況下,Java 開發人員並不須要在程序中顯式地使用類加載器,可是了 解類加載器的加載機制卻顯得相當重要。從如下幾個方面說:

  • 避 免 在 開 發 中 遇 到java.lang.ClassNotFoundException異 常 或 java.lang.NoClassDeFoundError異常時手足無措。只有瞭解類加載器的加載機制纔可以在出現異常的時候快速地根據錯誤異常日誌定位問題和解決問 題
  • 須要支持類的動態加載或須要對編譯後的字節碼文件進行加解密操做時,就 須要與類加載器打交道了
  • 開發人員能夠在程序中編寫自定義類加載器來從新定義類的加載規則,以便 實現一些自定義的處理邏輯

命名空間

何爲類的惟一性?

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確認其在 Java 虛擬機中的惟一性。每個類加載器,都擁有一個獨立的類名稱空間:比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義。 不然,即便這兩個類源自同一個 Class 文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,那這兩個類就一定不相等

命名空間

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

在大型應用中,咱們每每藉助這一特性,來運行同一個類的不一樣版本

代碼示例: 兩個不一樣的類加載器示例,獲取同一個類的 Class對象, 是不同的兩個

public class User {
    private int id;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                '}';
    }
}
//自定義類加載器
public class UserClassLoader extends ClassLoader {
    private String rootDir;

    public UserClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 編寫findClass方法的邏輯
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 獲取類的class文件字節數組
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class對象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 編寫獲取class文件並轉換爲字節碼流的邏輯 * @param className * @return
     */
    private byte[] getClassData(String className) {
        // 讀取類文件的字節
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            // 讀取類文件的字節碼
            while ((len = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 類文件的徹底路徑
     */
    private String classNameToPath(String className) {
        return rootDir + "\\" + className.replace('.', '\\') + ".class";
    }

    public static void main(String[] args) {
        String rootDir = "D:\\dev\\workspace\\demo\\代碼\\JVMDemo1\\chapter04\\src\\";

        try {
            //建立自定義的類的加載器1
            UserClassLoader loader1 = new UserClassLoader(rootDir);
            Class clazz1 = loader1.findClass("com.atguigu.java.User");

            //建立自定義的類的加載器2
            UserClassLoader loader2 = new UserClassLoader(rootDir);
            Class clazz2 = loader2.findClass("com.atguigu.java.User");

            System.out.println(clazz1 == clazz2); //clazz1與clazz2對應了不一樣的類模板結構。
            System.out.println(clazz1.getClassLoader());
            System.out.println(clazz2.getClassLoader());

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}
複製代碼

類加載機制的基本特徵

一般類加載機制有三個基本特徵:

  • 雙親委派模型。但不是全部類加載都遵照這個模型,有的時候,啓動類加載 器 所 加 載 的 類 型 , 是 可 能 要 加 載 用 戶 代 碼 的 , 比 如 JDK 內 部 的 ServiceProvider/ServiceLoader 機制,用戶能夠在標準 API 框架上,提供自 己的實現,JDK 也須要提供些默認的參考實現。例如,Java 中 JNDI、JDBC、 文件系統、Cipher 等不少方面,都是利用的這種機制,這種狀況就不會用雙 親委派模型去加載,而是利用所謂的上下文加載器
  • 可見性,子類加載器能夠訪問父加載器加載的類型,可是反過來是不容許的。 否則,由於缺乏必要的隔離,咱們就沒有辦法利用類加載器去實現容器的邏 輯
  • 單一性,因爲父加載器的類型對於子加載器是可見的,因此父加載器中加載 過的類型,就不會在子加載器中重複加載。可是注意,類加載器"鄰居"間, 同一類型仍然能夠被加載屢次,由於相互並不可見

回到頂部

2. 類的加載器分類

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

從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類 類加載器,可是 Java 虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類 ClassLoader 的類加載器都劃分爲自定義類加載器。不管類加載器的類型如何劃 分,在程序中咱們最多見的類加載器結構主要是以下狀況:

1607872568001

  • 除了頂層的啓動類加載器外,其他的類加載器都應當有本身的"父類"加載器
  • 不一樣類加載器看似是繼承(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);
	}
}
複製代碼

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 參數,打印類的加載過程

2.2 : 擴展類加載器

擴展類加載器(Extension ClassLoader)

  • Java 語言編寫,由 sun.misc.Launcher$ExtClassLoader 實現
  • 繼承於 ClassLoader 類
  • 父類加載器爲啓動類加載器
  • 從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,或從 JDK 的安裝目錄 的 jre/lib/ext 子目錄下加載類庫。若是用戶建立的 JAR 放在此目錄下,也會自動由擴展類加載器加載

2.3 系統類加載器

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

  • Java 語言編寫,由 sun.misc.Launcher$AppClassLoader 實現
  • 繼承於 ClassLoader 類
  • 父類加載器爲擴展類加載器
  • 它負責加載環境變量 classpath 或系統屬性 java.class.path 指定路徑下的類 庫
  • 應用程序中的類加載器默認是系統類加載器
  • 它是用戶自定義類加載器的默認父加載器
  • 經過 ClassLoader 的 getSystemClassLoader() 方法能夠獲取到該類加載器

2.4 用戶自定義類加載器

  1. 在 Java 的平常應用程序開發中,類的加載幾乎是由上述 3 種類加載器相互 配合執行的。在必要時,咱們還能夠自定義類加載器,來定製類的加載方式
  2. 體現 Java 語言強大生命力和巨大魅力的關鍵因素之一即是,Java 開發者可 以自定義類加載器來實現類庫的動態加載,加載源能夠是本地的 JAR 包, 也能夠是網絡上的遠程資源
  3. 經過類加載器能夠實現很是絕妙的插件機制,這方面的實際應用案例不勝枚 舉。例如,著名的 OSGI 組件框架,再如 Eclipse 的插件機制。類加載器爲 應用程序提供了一種動態增長新功能的機制,這種機制無需從新打包發佈應 用程序就能實現
  4. 同時,自定義加載器可以實現應用隔離,例如 Tomcat、Spring 等中間件和 組件框架都在內部實現了自定義的加載器,並經過自定義加載器隔離不一樣的 組件模塊。這種機制比 C/C++ 程序要好太多,想不修改 C/C++ 程序就能 爲其新增功能,幾乎是不可能的,僅僅一個兼容性便能阻擋全部美好的設想
  5. 自定義類加載器一般須要繼承於 ClassLoader

2.5 測試不一樣的類加載器

每一個 Class 對象都會包含一個定義它的 ClassLoader 的一個引用 獲取 ClassLoader 的途徑

各個加載器的獲取方式:

//獲取系統該類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        //獲取擴展類加載器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
        //試圖獲取引導類加載器:失敗
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null
複製代碼

說明:

站在程序的角度看,引導類加載器與另外兩種類加載器(系統類加載器和擴展 類加載器)並非同一個層次意義上的加載器,引導類加載器是使用 C++ 語言 編寫而成的,而另外兩種類加載器則是使用 Java 語言編寫的。因爲引導類加載 器壓根兒就不是一個 Java 類,所以在 Java 程序中只能打印出空值

數組類的加載器:

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        
            //關於數組類型的加載:使用的類的加載器與數組元素的類的加載器相同
            String[] arrStr = new String[10];
            System.out.println(arrStr.getClass().getClassLoader());//null:表示使用的是引導類加載器

            ClassLoaderTest1[] arr1 = new ClassLoaderTest1[10];
System.out.println(arr1.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2

            int[] arr2 = new int[10];
            System.out.println(arr2.getClass().getClassLoader());//null:不須要類的加載器
            System.out.println(Thread.currentThread().getContextClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

說明:

數組類的 Class 對象,不是由類加載器去建立的,而是在 Java 運行期 JVM 根據須要自動建立的。對於數組類的類加載器來講,是經過 Class.geetClassLoader() 返回的,與數組當中元素類型的類加載器是同樣的:如 果數組當中的元素類型是基本數據類型,數組類是沒有類加載器的,由於基本類型不須要加載器加載

回到頂部

3. 類的加載器源碼分析

ClassLoader 與現有類加載的關係:

1607957911296

除了以上虛擬機自帶的加載器外,用戶還能夠定製本身的類加載器。Java 提 供了抽象類 java.lang.ClassLoader,全部用戶自定義的類加載器都應該繼承 ClassLoader 類

ClassLoader的主要方法

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

  • public final ClassLoader getParent()

    返回該類加載器的"父類"加載器

  • public Class loadClass(String name) throws ClassNotFoundException

    加載全類名稱爲 name 的類,返回結果爲 java.lang.Class 類的實例。若是找不到 類,則返回 ClassNotFountException 異常。該方法中的邏輯就是雙親委派模式的 實現

    源碼: 體現出雙親委派機制

    //參數 var2 表明是否須要解析,默認false
    protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
        //同步加鎖,保證只加載一次
            synchronized(this.getClassLoadingLock(var1)) {
                //首先,在緩存中判斷是否已經加載同名類
                Class var4 = this.findLoadedClass(var1);
                //不爲空直接返回
                if (var4 == null) {
                    long var5 = System.nanoTime();
    
                    try {
                        //若是父類加載器不爲空,則調用父類的加載,遞歸,若是返回空,則繼續執行
                        if (this.parent != null) {
                            var4 = this.parent.loadClass(var1, false);
                        } else {
                            //若是父類的加載器爲空,則表明爲啓動類加載器,使用啓動類加載器加載
                            var4 = this.findBootstrapClassOrNull(var1);
                        }
                    } catch (ClassNotFoundException var10) {
                        ;
                    }
    			  //若是沒能加載此類, 則 說明啓動類加載器沒能加載,由其上一層加載器加載,若是仍然返回空
                    //則繼續向上,
                    if (var4 == null) {
                        long var7 = System.nanoTime();
                        //若是不是啓動類加載器加載類,最終是使用findClass方法加載類
                        var4 = this.findClass(var1);
                        PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                        PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                        PerfCounter.getFindClasses().increment();
                    }
                }
    
                if (var2) {
                    this.resolveClass(var4);
                }
    
                return var4;
            }
        }
    複製代碼
  • protected Class findClass(String name) throws ClassNotFoundException

    • 查找二進制名稱爲 name 的類,返回結果爲 java.lang.Class 類的實例。這是 一個受保護的方法,JVM 鼓勵咱們重寫此方法,須要自定義加載器遵循雙親委派機制,該方法會在檢查完父類加載器以後被 loadClass() 方法調用 (loadClass方法體現雙親委派機制,不建議重寫,而此方法則是使用類路徑二進制流加載類)
    • 在 JDK 1.2 以前,在自定義類加載時,總會去繼承 ClassLoader 類並重寫 loadClass 方法,從而實現自定義的類加載類。可是在 JDK 1.2 以後已再也不建議 用戶去覆蓋 loadClass() 方法,而是建議把自定義的類加載邏輯寫在 find Class() 方法中,從前面的分析可知,findClass() 方法是在 loadClass() 方法中被調用的, 當 loadClass() 方法中父加載器加載失敗後,則會調用本身的 findClass() 方法來 完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委派機制
    • 須要注意的是 ClassLoader 類中並無實現 findClass() 方法的具體代碼邏 輯,取而代之的是拋出 ClassNotFoundException 異常,同時應該知道的是 findClass() 方法一般是和 defineClass() 方法一塊兒使用的。通常狀況下,在自定義類加載器時,會直接覆蓋 ClassLoader 的 findClass() 方法並編寫加載規則, 取得要加載類的字節碼後轉換成流,而後調用 defineClass() 方法生成類的 Class 對象 (findClass底層就是defineClass方法)
  • protected final Class defineClass(String name, byte[] b, int off, int len)

    • 根據給定的字節數組 b 轉換爲 Class 的實例,off 和 len 參數表示實際 Class 信息在 byte 數組中的位置和長度,其中 byte 數組 b 是 ClassLoader 從 外部獲取的。這是受保護的方法,只有在自定義 ClassLoader 子類中可使用

    • defineClass() 方法是用來將 byte 字節流解析成 JVM 可以識別的 Class 對 象(ClassLoader 中已實現該方法邏輯),經過這個方法不只可以經過 Class 文件 實例化 Class 對象,也能夠經過其它方式實例化 Class 對象,如經過網絡中接 收一個類的字節碼,而後轉換爲 byte 字節流建立對應的 Class 對象

    • //findClass 和 defineClass 方法的關係
        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)

    連接指定的一個 Java 類。使用該方法可使用類的 Class 對象建立完成的 同時也被解析。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內 存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用

  • protected final Class findLoadedClass(String name)

    查找名稱爲 name 的已經被加載過的類,返回結果爲 java.lang.Class 類的實 例。這個方法是 final 方法,沒法被修改

  • private final ClassLoader parent;

    它也是一個 ClassLoader 的實例,這個字段所表示的 ClassLoader 也稱爲這個 ClassLoader 的雙親。在類加載的過程當中,ClassLoader 可能會將某些請求交予本身的雙親處理

加載器的繼承關係

根據 加載器的 繼承關係圖能夠看到

1607957911296

擴展類加載器和系統類加載器,都是繼承的URLClassLoader, -> SecureClassLoader

SecureClassLoader 與 URLClassLoader

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

前面說過,ClassLoader 是一個抽象類,不少方法是空的沒有實現,好比 findClass()、findResource() 等。而 URLClassLoader 這個實現類爲這些方法提供 了具體的實現。並新增了 URLClassPath 類協助取得 Class 字節碼流等功能。 在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承 URLClassLoader 類,這樣就能夠避免本身去編寫 findClass() 方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔

ExtClassLoader 與 AppClassLoader

ExtClassLoader 並無重寫 loadClass() 方法,這足以說明其遵循雙親委派模式,而 AppClassLoader 重載了 loadClass() 方法,但最終調用的仍是父類 loadClass() 方法,所以依然遵循雙親委派模式

Class.forName() 與 ClassLoader.loadClass()

  • Class.forName() : 是 一 個 靜 態 方 法 , 最 常 用 的 是 Class.forName(String className);根據傳入的類的權限定名返回一個 Class 對象。**該方法在將 Class 文件加載到內存的同時,會執行類的初始化。**如: Class.forName("com.atguigu.java.HelloWorld");
  • ClassLoader.loadClass() 這是一個實例方法,須要一個 ClassLoader 對象來調用該方法。該方法將 Class 文件加載到內存時,並不會執行類的初始化,(根據上面的源代碼能夠看出也沒有進行解析操做,僅僅只是將類加載)直 到 這 個 類 第 一 次 使 用 時 才 進 行 初 始 化 。 該 方 法 因 爲 需 要 得 到 一 個 ClassLoader 對象,因此能夠根據須要指定使用哪一個類加載器,如: ClassLoader c1 = .....; c1.loadClass("com.atguigu.java.HelloWorld");

回到頂部

4. 雙親委派機制

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

定義:

若是一個類加載器在接到加載類的請求時,它首先不會本身嘗試去加載這個 類,而是把這個請求任務委託給父類加載器去完成,依次遞歸,若是父類加載器 能夠完成類加載任務,就成功返回。只有父類加載器沒法完成此加載任務時,才 本身去加載

本質:

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

雙親委派機制簡圖和流程圖以下:

1608038976645

1608039276178

4.1 優點與劣勢

雙親委派機制優點 :

  • 避免類的重複加載,確保一個類的全局惟一性 ,Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層級關係,經過這種 層級關係能夠避免類的重複加載,當父親已經加載了該類時,就沒有必要子 ClassLoader 再加載一次 好比此時我手動使用各個加載器去加載同一個類, 可是由於雙親委派機制,最終爲同一個加載器加載,同一個命名空間,不會重複加載
  • 保護程序安全,防止核心 API 被隨意篡改 , 好比自定義一個 java.lang.String 的類, 就算引用了此類,也會由於雙親委派機制,直接被啓動類加載器,就加載了 原生的String類,保護了核心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.2 破壞雙親委派機制

雙親委派模型並非一個具備強制性約束的模型,而是 Java 設計者推薦給 開發者們的類加載器實現方式

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

第一次破壞雙親委派機制: JDK1.2以前自己就沒有實現雙親委派機制

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

因爲雙親委派模型在 JDK 1.2 以後才被引入,可是類加載器的概念和抽象類 java.lang.ClassLoader 則在 Java 的第一個版本中就已經存在,面對已經存在的 用戶自定義類加載器的代碼,Java 設計者們引入雙親委派模型時不得不作出一 些妥協,爲了兼容這些已有的代碼,沒法再以技術手段避免 loadClass() 被子類 覆蓋的可能性,只能在 JDK 1.2 以後的 java.lang.ClassLoader 中添加一個新的 protected 方法 findClass(),並引導用戶編寫的類加載邏輯時儘量去重寫這個 方法,而不是在 loadClass() 中編寫代碼。上節咱們已經分析過 loadClass() 方法, 雙親委派的具體邏輯就實如今這裏面,按照 loadClass() 方法的邏輯,若是父類 加載失敗,會自動調用本身的 findClass() 方法來完成加載,這樣既不影響用戶 按照本身的意願去加載類,又能夠保證新寫出來的類加載器是符合雙親委派規則 的

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

雙親委派模型的第二次"被破壞"是由這個模型自身的缺陷致使的,雙親委派 很好地解決了各個類加載器協做時基礎類型的一致性問題(越基礎的類由越上層 的加載器進行加載),基礎類型之因此被稱爲"基礎",是由於它們老是做爲被用戶代碼繼承、調用的 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 的加載 提供了一種相對合理的解決方案

結構圖:

1608041143881

默認上下文加載器就是應用類加載器,這樣以上下文加載器爲中介,使得啓 動類加載器中的代碼也能夠訪問應用類加載器中的類

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

雙親委派模型的第三次"被破壞"是因爲用戶對程序動態性的追求而致使的。 如:代碼熱替換(Hot Swap)、**模塊熱部署(Hot Deployment)**等

IBM 公司主導的 JSR-291(即 OSGI R4.2)實現模塊化熱部署的關鍵是它自 定義的類加載器機制的實現,每一個程序模塊(OSGI 中稱爲 Bundle)都有一個本身 的類加載器,當須要更換一個 Bundle 時,就把 Bundle 連同類加載器一塊兒換掉 以實現代碼的熱替換。在 OSGI 環境下,類加載器再也不雙親委派模型推薦的樹 狀結構,而是進一步發展爲更加複雜的網狀結構 當收到類加載請求時,OSGI 將按照下面的順序進行類搜索:

  • 將以 java. 開頭的類,委派給父類加載器加載
  • 不然,將委派列表名單內的類,委派給父類加載器加載
  • 不然,將 Import 列表中的類,委派給 Export 這個類的 Bundle 的類加載器 加載
  • 不然,查找當前 Bundle 的 ClassPath,使用本身的類加載器加載
  • 不然,查找類是否在本身的 Fragment Bundle 中,若是在,則委派給 Fragment Bundle 的類加載器加載
  • 不然,查找 Dynamic Import 列表的 Bundle,委派給對應 Bundle 的類加載 器加載
  • 不然,類查找失敗

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

小結:

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

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

4.3 熱替換的代碼實現

熱替換是指在程序運行過程當中,不中止服務,只經過替換程序文件來修改程 序的行爲。熱替換的關鍵需求在於服務不能中斷,修改必須當即表現正在運行的 系統之中。基本上大部分腳本語言都是天生支持熱替換的,好比:PHP,只要替 換了 PHP 源文件,這種改動就會當即生效,而無需重啓 Web 服務器

但對 Java 來講,熱替換並不是天生就支持,若是一個類已經加載到系統中, 經過修改類文件,並沒有法讓系統再來加載並重定義這個類。

所以,在 Java 中實 現這一功能的一個可行的方法就是靈活運用 ClassLoader

**注意:由不一樣 ClassLoader 加載的同名類屬於不一樣的類型,不能相互轉換和兼容。即兩個不一樣的 ClassLoader 加載同一個類,在虛擬機內部,會認爲這 2 個類是徹底不一樣的 **

根據這個特色,能夠用來模擬熱替換的實現,基本思路以下圖所示:

1608041857469

代碼實現:

將下面這個類使用javac編譯爲class文件

public class Demo1 {
    //原始的方法打印
    public void hot() {
        System.out.println("OldDemo1");
    }
}
複製代碼

自定義的ClassLoader, 無需過多關注

public class MyClassLoader extends ClassLoader {
    private String rootDir;

    public MyClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(className);
        FileChannel fileChannel = null;
        WritableByteChannel outChannel = null;
        if (null == clazz) {
            try {
                String classFile = getClassFile(className);
                FileInputStream fis = new FileInputStream(classFile);
                fileChannel = fis.getChannel();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                outChannel = Channels.newChannel(baos);
                ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                while (true) {
                    int i = fileChannel.read(buffer);
                    if (i == 0 || i == -1) {
                        break;
                    }
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }

                byte[] bytes = baos.toByteArray();
                clazz = defineClass(className, bytes, 0, bytes.length);


            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileChannel != null)
                        fileChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    if (outChannel != null)
                        outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return clazz;
    }

    /**
     * 類文件的徹底路徑
     */
    private String getClassFile(String className) {
        return rootDir + "\\" + className.replace('.', '\\') + ".class";
    }
}
複製代碼

使用自定義類加載加載類,並使用

public class LoopRun {
    public static void main(String args[]) {
        //循環調用Demo1 類的方法,打印
        while (true) {
            try {
                //1. 每次使用類的方法時,都建立自定義類加載器的實例,去加載
                MyClassLoader loader = new MyClassLoader("D:\\code\\workspace_idea5\\JVMDemo1\\chapter04\\src\\");
                //2. 加載指定的類,因爲每次都是不用的類加載器加載的,因此會重複加載爲新的類
                Class clazz = loader.findClass("com.atguigu.java1.Demo1");
                //3. 建立運行時類的實例
                Object demo = clazz.newInstance();
                //4. 獲取運行時類中指定的方法
                Method m = clazz.getMethod("hot");
                //5. 調用指定的方法
                m.invoke(demo);
                Thread.sleep(5000);
            } catch (Exception e) {
                System.out.println("not find");

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }

            }
        }
    }

}
複製代碼

在運行時修改Demo1類方法,並從新編譯,打印結果:OldDemo1

OldDemo1
OldDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
OldDemo1---> NewDemo1
複製代碼

回到頂部

5. 沙箱安全機制

做用:

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

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

沙箱機制就是將 Java 代碼**限定在虛擬機(JVM)特定的運行範圍中,而且 嚴格限制代碼對本地系統資源訪問。**經過這樣的措施來保證對代碼的有限隔離, 防止對本地系統形成破壞

沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU、內存、文件系統、 網絡。不一樣級別的沙箱對這些資源訪問的限制也能夠不同 全部的 Java 程序運行均可以指定沙箱,能夠定製安全策略

  1. JDK 1.0 時期

    在 Java 中將執行程序分紅本地代碼和遠程代碼兩種,本地代碼默認視爲可 信任的,而遠程代碼則被看做是不受信的。對於授信的本地代碼,能夠訪問一切 本地資源。而對於非授信的遠程代碼在早期的 Java 實現中,安全依賴於沙箱 (Sandbox)機制。以下圖所示 JDK 1.0 安全模型

1608042686193

  1. JDK 1.1 時期

    JDK 1.0 中如此嚴格的安全機制也給程序的功能擴展帶來障礙,好比當用戶 但願遠程代碼訪問本地系統的文件時候,就沒法實現

    所以在後續的 JDK 1.1 版本中,針對安全機制作了改進,增長了安全策略。 容許用戶指定代碼對本地資源的訪問權限

    以下圖所示 JDK 1.1 安全模型

1608042733658

  1. JDK 1.2 時期

    在 JDK 1.2 版本中,再次改進了安全機制,增長了代碼簽名。不論本地代碼 或是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限 不一樣的運行空間,來實現差別化的代碼執行權限控制。以下圖所示 JDK 1.2 安 全模型:

    1608042767725

  2. JDK 1.6 時期

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

    虛擬機會把全部代碼加載到不一樣的系統域和應用域。系統域部分專門負責與 關鍵資源進行交互,而各個應用域部分則經過系統域的部分代理來對各類須要的 資源進行訪問。虛擬機中不一樣的受保護域(Protected Domain),對應不同的權限 (Permission)。存在於不一樣域中的類文件就具備了當前域的所有權限,以下圖所 示,最新的安全模型(JDK 1.6)

    1608042806533

回到頂部

6. 自定義類加載器

爲何要自定義類加載器?

  • 隔離加載類

    在某些框架內進行中間件與應用的模塊隔離,把類加載到不一樣的環境。好比: 阿里內某容器框架經過自定義類加載器確保應用中依賴的 jar 包不會影響到中 間件運行時使用的 jar 包。再好比:Tomcat 這類 Web 應用服務器,內部自定 義了好幾種類加載器,用於隔離同一個 Web 應用服務器上的不一樣應用程序。(類 的仲裁 --> 類衝突)

  • 修改類加載的方式

    類的加載模型並不是強制,除 Bootstrap 外,其餘的加載並不是必定要引入,或 者根據實際狀況在某個時間點按需進行動態加載

  • 擴展加載源

好比從數據庫、網絡、甚至是電視機機頂盒進行加載

  • 防止源碼泄露

Java 代碼容易被編譯和篡改,能夠進行編譯加密。那麼類加載也須要自定義, 還原加密的字節碼

常見的場景

實現相似進程內隔離,類加載器實際上用做不一樣的命名空間,以提供相似容 器、模塊化的效果。例如,兩個模塊依賴於某個類庫的不一樣版本,若是分別被不 同的容器加載,就能夠互不干擾。這個方面的集大成者是 Java EE 和 OSGI、 JPMS 等框架

應用須要從不一樣的數據源獲取類定義信息,例如網絡數據源,而不是本地文 件系統。或者是須要本身操縱字節碼,動態修改或者生成類型

注意:

在通常狀況下,使用不一樣的類加載器去加載不一樣的功能模塊,會提升應用程 序的安全性。可是,若是涉及 Java 類型轉換,則加載器反而容易產生不美好的 事情。在作 Java 類型轉換時,只有兩個類型都是由同一個加載器所加載,才能進行類型轉換,不然轉換時會發生異常

代碼實現

用戶經過定製本身的類加載器,這樣能夠從新定義類的加載規則,以便實現 一些自定義的處理邏輯

實現方式

  • Java 提供了抽象類 java.lang.ClassLoader,全部用戶自定義的類加載器都應該繼承 ClassLoader 類 (或其子類)
  • 在自定義 ClassLoader 的子類時候,咱們常見的會有兩種作法:
    • 方式一:重寫 loadClass() 方法
    • 方式二:重寫 findClass() 方法 (推薦)

對比:

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

  • loadClass() 這個方法是實現雙親委派模型邏輯的地方,擅自修改這個方法會 致使模型被破壞,容易形成問題。所以咱們最好是在雙親委派模型框架內進 行小範圍的改動,不破壞原有的穩定結構。同時,也避免了本身重寫 loadClass() 方法的過程當中必須寫雙親委託的重複代碼,從代碼的複用性來看, 不直接修改這個方法始終是比較好的選擇
  • 當編寫好自定義類加載器後,即可以在程序中調用 loadClass() 方法來實現 類加載操做

說明 :

  • 其父類加載器是系統類加載器
  • JVM 中的全部類加載都會使用 java.lang.ClassLoader.loadClass(String) 接口 (自定義類加載器並重寫 java.lang.ClassLoader.loadClass 接口的除外), 連 JDK 的核心類庫也不能例外

代碼實現:

public class MyClassLoader extends ClassLoader {
    // class文件的存放路徑
    private String byteCodePath;

    public MyClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }

    public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.byteCodePath = byteCodePath;
    }

    
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //獲取字節碼文件的完整路徑
            String fileName = byteCodePath + className + ".class";
            //獲取一個輸入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //獲取一個輸出流
            baos = new ByteArrayOutputStream();
            //具體讀入數據並寫出的過程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //獲取內存中的完整的字節數組的數據
            byte[] byteCodes = baos.toByteArray();
            //調用defineClass(),將字節數組的數據轉換爲Class的實例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}
複製代碼

使用:

public class MyClassLoaderTest {
    public static void main(String[] args) {
        MyClassLoader loader = new MyClassLoader("D:\\dev\\workspace\\demo\\代碼\\JVMDemo1\\chapter04\\src\\com\\atguigu\\java1\\");

        try {
            Class clazz = loader.loadClass("Demo1");
            System.out.println("加載此類的類的加載器爲:" + clazz.getClassLoader().getClass().getName());

            System.out.println("加載當前Demo1類的類的加載器的父類加載器爲:" + clazz.getClassLoader().getParent().getClass().getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

打印結果:

加載此類的類的加載器爲:com.atguigu.java2.MyClassLoader //使用成功
加載當前Demo1類的類的加載器的父類加載器爲:sun.misc.Launcher$AppClassLoader //說明自定義加載器的默認父類就是系統加載器
複製代碼

回到頂部

7. JDK9 新特性

爲了保證兼容性,JDK 9 沒有從根本上改變三層類加載器架構和雙親委派模 型,但爲了模塊化系統的順利運行,仍然發生了一些值得被注意的變更

  1. 擴展機制被移除,擴展類加載器因爲向後兼容性的緣由被保留,不過被重命 名爲平臺類加載器(Platform Class Loader)。能夠經過 ClassLoader 的新方法 getPlatformClassLoader() 來獲取

    JDK 9 時基於模塊化進行構建(原來的 rt.jar 和 tools.jar 被拆分紅數十個 JMOD 文件),其中的 Java 類庫就已自然地知足了可擴展的需求,那天然無需 再保留 \lib\ext 目錄,此前使用這個目錄或者 java.ext.dirs 系統 變量來擴展 JDK 功能的機制已經沒有繼續存在的價值了

  2. 平臺類加載器和應用程序類加載器都再也不繼承自 java.net.URLClassLoader

    如今啓動類加載器、平臺類加載器、應用程序類加載器全都繼承於 jdk.internal.loader.BuiltinClassLoader

    1608044427731

    若是有程序直接依賴了這種繼承關係,或者依賴了 URLClassLoader 類的特 定方法,那代碼極可能會在 JDK 9 及更高版本的 JDK 中崩潰

  3. 在 Java 9 中,類加載器有了名稱。該名稱在構造方法中指定,能夠經過 getName() 方法來獲取。平臺類加載器的名稱是 Platform,應用類加載器的 名稱是 App。類加載器的名稱在調試與類加載器相關的問題時會很是有用

  4. 啓動類加載器如今是在 JVM 內部和 Java 類庫共同協做實現的類加載器 (之前是 C++ 實現),但爲了與以前代碼兼容,在獲取啓動類加載器的場景 中仍然會返回 null,而不會獲得 BootClassLoader 實例

  5. 類加載的委派關係也發生了變更

    當平臺及應用程序類加載器收到類加載請求,在委派給父加載器加載前,要 先判斷該類是否可以歸屬到某一個系統模塊中,若是能夠找到這樣的歸屬關係, 就要優先委派給負責哪一個模塊的加載器完成加載

1608044530915

附加信息:

在 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
    複製代碼
相關文章
相關標籤/搜索