【JVM進階之路】十四:類載入器和類載入機制

三分惡發表於2021-06-01

在上一章裡,我們已經學習了類載入的過程,我們知道在載入階段需要”通過一個類的全限定名來獲取描述該類的二進位制位元組流“,而來完成這個工作的就是類載入器(Class Loader)。

1、類與類載入器

類載入器只用於實現類的載入動作。

但對於任意一個類,都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器中的唯一性,每 一個類載入器,都擁有一個獨立的類名稱空間。

類載入器和類確定類是否相等

這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個Java虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等。

如下演示了不同的類載入器對instanceof關鍵字運算的結果的影響。

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        //自定義一個簡單的類載入器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            //載入類方法
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    //獲取檔名
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    //載入輸入流
                    InputStream is = getClass().getResourceAsStream(fileName);
                    //使用父類載入
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    //從流中轉化類的例項
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        //使用自己實現的類載入器載入
        Object obj = myLoader.loadClass("cn.fighter3.loader.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        //例項判斷
        System.out.println(obj instanceof cn.fighter3.loader.ClassLoaderTest);
    }
}

執行結果:

執行結果

在程式碼裡定義了一個簡單的類載入器,使用這個類載入器去載入cn.fighter3.loader.ClassLoaderTest類並建立例項,去做型別檢查的時候,發現結果是false。

2、雙親委派模型

從Java虛擬機器的角度來看,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器使用C++語言實現,是虛擬機器自身的一部分;另外一種就是其他所有的類載入器,這些類載入器都由Java語言實現,獨立存在於虛擬機器外部,並且全都繼承自抽象類 java.lang.ClassLoader。

站在Java開發人員的角度來看,類載入器就應當劃分得更細緻一些。自JDK 1.2以來,Java一直保持著三層類載入器、雙親委派的類載入架構。

雙親委派模型

雙親委派模型如上圖:

  • 啟動類載入器(Bootstrap Class Loader):負責載入存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,能被Java虛擬機器能夠識別的(按照檔名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類。
  • 擴充套件類載入器(Extension Class Loader):負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。
  • 應用程式類載入器(Application Class Loader):負責載入使用者類路徑 (ClassPath)上所有的類庫,如果沒有自定義類載入器,一般情況下這個載入器就是程式中預設的類載入器。

使用者還可以加入自定義的類載入器器來進行擴充套件。

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

雙親委派機制

為什麼要用雙親委派機制呢?

答案是為了保證應用程式的穩定有序。

例如類java.lang.Object,它存放在rt.jar之中,通過雙親委派機制,保證最終都是委派給處於模型最頂端的啟動類載入器進行載入,保證Object的一致。反之,都由各個類載入器自行去載入的話,如果使用者自己也編寫了一個名為java.lang.Object的類,並放在程式的 ClassPath中,那系統中就會出現多個不同的Object類。

雙親委派模型的程式碼實現非常簡單,在java.lang.ClassLoader.java中有一個 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 {
                    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);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

3、破壞雙親委派模型

雙親委派機制在歷史上主要有三次破壞:

第一次破壞

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

由於雙親委派模型在JDK 1.2之後才被引入,但是類載入器的概念和抽象類 java.lang.ClassLoader則在Java的第一個版本中就已經存在,為了向下相容舊程式碼,所以無法以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之後的java.lang.ClassLoader中新增一個新的 protected方法findClass(),並引導使用者編寫的類載入邏輯時儘可能去重寫這個方法,而不是在 loadClass()中編寫程式碼。

重寫loadClass破壞雙親委派

第二次破壞

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,如果有基礎型別又要呼叫回使用者的程式碼,那該怎麼辦呢?

例如我們比較熟悉的JDBC:

各個廠商各有不同的JDBC的實現,Java在核心包\lib裡定義了對應的SPI,那麼這個就毫無疑問由啟動類載入器載入器載入。

但是各個廠商的實現,是沒辦法放在核心包裡的,只能放在classpath裡,只能被應用類載入器載入。那麼,問題來了,啟動類載入器它就載入不到廠商提供的SPI服務程式碼。

為了解決這個我呢提,引入了一個不太優雅的設計:執行緒上下文類載入器 (Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。

JNDI服務使用這個執行緒上下文類載入器去載入所需的SPI服務程式碼,這是一種父類載入器去請求子類載入器完成類載入的行為。

載入第三方spi第二次破壞

第三次破壞

雙親委派模型的第三次“被破壞”是由於使用者對程式動態性的追求而導致的,例如程式碼熱替換(Hot Swap)、模組熱部署(Hot Deployment)等。

OSGi實現模組化熱部署的關鍵是它自定義的類載入器機制的實現,每一個程式模組(OSGi中稱為 Bundle)都有一個自己的類載入器,當需要更換一個Bundle時,就把Bundle連同類載入器一起換掉以實現程式碼的熱替換。在OSGi環境下,類載入器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加複雜的網狀結構。


"簡單的事情重複做,重複的事情認真做,認真的事情有創造性地做!"——

我是三分惡,可以叫我老三/三分/三哥/三子,一個能文能武的全棧開發,我們們下期見!




參考:

【1】:《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第3版) 》

【4】:讀者美團五面:Java歷史上有三次破壞雙親委派模型,是哪三次?

相關文章