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


在上一章里,我們已經學習了類加載的過程,我們知道在加載階段需要”通過一個類的全限定名來獲取描述該類的二進制字節流“,而來完成這個工作的就是類加載器(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歷史上有三次破壞雙親委派模型,是哪三次?


免責聲明!

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



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