首頁 > 軟體

JVM的類載入器和雙親委派模式你瞭解嗎

2022-03-13 22:00:09

類載入器

Java虛擬機器器設計團隊有意把類載入階段中的“通過一個類的全限定名來獲取描述該類的二進位制位元組流”這個動作放到Java虛擬機器器外部去實現,以便讓應用程式自己決定如何去獲取所需的類。實現這個動作的程式碼被稱為“類載入器”(ClassLoader)。

對於任意一個類,都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個Java虛擬機器器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等

名稱載入的類說明
Bootstrap ClassLoader(啟動類載入器)JAVA_HOME/jre/lib無法直接存取
Extension ClassLoader(拓展類載入器)JAVA_HOME/jre/lib/ext上級為Bootstrap,顯示為null
Application ClassLoader(應用程式類載入器)classpath上級為Extension
自定義類載入器自定義上級為Application

1、啟動類載入器

可通過在控制檯輸入指令,使得類被啟動類加器載入,它是用C++寫的,看不到原始碼;其他類載入器是用Java寫的,說白了就是一些Java類,比如擴充套件類載入器、應用類載入器。

//查詢所有被啟動類載入器載入的類
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}
//查詢到的結果
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/resources.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/rt.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jsse.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jce.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/charsets.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/jfr.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/classes

由上可以看出啟動類載入的都是jre和jre/lib目錄下的核心庫,具體路徑要看你的jre安裝在哪裡

2、拓展類載入器

如果classpath和JAVA_HOME/jre/lib/ext 下有同名類,載入時會使用拓展類載入器載入。當應用程式類載入器發現拓展類載入器已將該同名類載入過了,則不會再次載入

URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dns_sd.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/zipfs.jar

這些類庫具體是什麼不重要,只需要知道不同的類庫可能是被不同的類載入器載入的。

3、應用程式類載入器

URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}
file:/{專案工程目錄}/bin/

這是當前java工程的bin目錄,也就是我們自己的Java程式碼編譯成的class檔案所在。

4、雙親委派模式

雙親委派模式,呼叫類載入器ClassLoader 的 loadClass 方法時,查詢類的規則。

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 {
                //看是否被它的上級載入器載入過了 Extension的上級是Bootstarp,但它顯示為null
                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方法去找到該類,如果還是沒找到,就丟擲異常
                //然後讓應用類載入器去找classpath下找該類
                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;
    }
}

有一個描述類載入器載入類過程的術語:雙親委派模型。然而這是一個很有誤導性的術語,它應該叫做單親委派模型(Parent-Delegation Model)。但是沒有辦法,大家都已經這樣叫了。所謂雙親委派,這個親就是指ClassLoader裡的全域性變數parent,也就是父載入器。

雙親委派的具體過程如下:

  • 當一個類載入器接收到類載入任務時,先查快取裡有沒有,如果沒有,將任務委託給它的父載入器去執行。
  • 父載入器也做同樣的事情,一層一層往上委託,直到最頂層的啟動類載入器為止。
  • 如果啟動類載入器沒有找到所需載入的類,便將此載入任務退回給下一級類載入器去執行,而下一級的類載入器也做同樣的事情。
  • 如果最底層類載入器仍然沒有找到所需要的class檔案,則丟擲異常。
  • 所以是一條線傳上再傳下,並沒有什麼“雙親”。

為什麼要雙親委派?

:確保類的全域性唯一性。

如果你自己寫的一個類與核心類庫中的類重名,會發現這個類可以被正常編譯,但永遠無法被載入執行。因為你寫的這個類不會被應用類載入器載入,而是被委託到頂層,被啟動類載入器在核心類庫中找到了。如果沒有雙親委託機制來確保類的全域性唯一性,誰都可以編寫一個java.lang.Object類放在classpath下,那應用程式就亂套了。

從安全的角度講,通過雙親委託機制,Java虛擬機器器總是先從最可信的Java核心API查詢型別,可以防止不可信的類假扮被信任的類對系統造成危害。

5、自定義類載入器

如果我們自己去實現一個類載入器,基本上就是繼承ClassLoader之後重寫findClass方法,且在此方法的最後調包defineClass。

5.1、使用場景

  • 想載入非 classpath 隨意路徑中的類檔案
  • 通過介面來使用實現,希望解耦時,常用在框架設計
  • 這些類希望予以隔離,不同應用的同名類都可以載入,不衝突,常見於 tomcat 容器

5.2、步驟

  • 繼承ClassLoader父類別
  • 要遵從雙親委派機制,重寫 findClass 方法
    • 不是重寫loadClass方法,否則不會走雙親委派機制
  • 讀取類檔案的位元組碼
  • 呼叫父類別的 defineClass 方法來載入類
  • 使用者呼叫該類載入器的 loadClass 方法
protected Class<?> findClass(final String name) throws ClassNotFoundException {
    // 1、安全檢查
    // 2、根據絕對路徑把硬碟上class檔案讀入記憶體
    byte[] raw = getBytes(name); 
    // 3、將二進位制資料轉換成class物件
    return defineClass(raw);
}

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容! 


IT145.com E-mail:sddin#qq.com