JAVA 雙親委派與類加載器

JAVA 雙親委派與類加載器

雙親委派

虛擬機在加載類的過程中需要使用類加載器進行加載,而在Java中,類加載器有很多,那麼當JVM想要加載一個.class文件的時候,到底應該由哪個類加載器加載呢?

這就不得不提到”雙親委派機制”。

首先,我們需要知道的是,Java語言系統中支持以下4種類加載器:

  • Bootstrap ClassLoader 啓動類加載器
  • Extention ClassLoader 標準擴展類加載器
  • Application ClassLoader 應用類加載器
  • User ClassLoader 用戶自定義類加載器

這四種類加載器之間,是存在着一種層次關係的,如下圖

-w704

一般認爲上一層加載器是下一層加載器的父加載器,那麼,除了BootstrapClassLoader之外,所有的加載器都是有父加載器的。

那麼,所謂的雙親委派機制,指的就是:當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類,而是把這個請求委託給自己的父加載器去加載。只有父加載器無法加載這個類的時候,纔會由當前這個加載器來負責類的加載。

那麼,什麼情況下父加載器會無法加載某一個類呢?

其實,Java中提供的這四種類型的加載器,是有各自的職責的:

  • Bootstrap ClassLoader ,主要負責加載Java核心類庫,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader,主要負責加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。
  • Application ClassLoader ,主要負責加載當前應用的classpath下的所有類
  • User ClassLoader , 用戶自定義的類加載器,可加載指定路徑的class文件

爲什麼使用雙親委派

通過委派的方式,可以避免類的重複加載,當父加載器已經加載過某一個類時,子加載器就不會再重新加載這個類。

另外,通過雙親委派的方式,還保證了安全性。因爲Bootstrap ClassLoader在加載的時候,只會加載JAVA_HOME中的jar包裏面的類,如java.lang.Integer,那麼這個類是不會被隨意替換的,除非有人跑到你的機器上, 破壞你的JDK。

那麼,就可以避免有人自定義一個有破壞功能的java.lang.Integer被加載。這樣可以有效的防止核心Java API被篡改。

“父子加載器”之間的關係是繼承嗎?

很多人看到父加載器、子加載器這樣的名字,就會認爲Java中的類加載器之間存在着繼承關係。

甚至網上很多文章也會有類似的錯誤觀點。

這裏需要明確一下,雙親委派模型中,類加載器之間的父子關係一般不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼的。

如下爲ClassLoader中父加載器的定義:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;
}

雙親委派的實現

實現雙親委派的代碼都集中在java.lang.ClassLoaderloadClass()方法之中

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

主要就是以下幾個步驟:

  1. 先檢查類是否已經被加載過
  2. 若沒有加載則調用父加載器的loadClass()方法進行加載
  3. 若父加載器爲空則默認使用啓動類加載器作爲父加載器。
  4. 如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

如何主動破壞雙親委派機制?

知道了雙親委派模型的實現,那麼想要破壞雙親委派機制就很簡單了。

因爲他的雙親委派過程都是在loadClass方法中實現的,那麼想要破壞這種機制,那麼就自定義一個類加載器,重寫其中的loadClass方法,使其不進行雙親委派即可。

loadClass()、findClass()、defineClass()區別

ClassLoader中和類加載有關的方法有很多,前面提到了loadClass,除此之外,還有findClassdefineClass等,那麼這幾個方法有什麼區別呢?

  • loadClass()
    • 就是主要進行類加載的方法,默認的雙親委派機制就實現在這個方法中。
  • findClass()
    • 根據名稱或位置加載.class字節碼
  • definclass()
    • 把字節碼轉化爲Class

如果你想定義一個自己的類加載器,並且要遵守雙親委派模型,那麼可以繼承ClassLoader,並且在findClass中實現你自己的加載邏輯即可。

JDBC 加載SPI接口實現類

JDBC中DrvierManager

如典型的JDBC服務,我們通常通過以下方式創建數據庫連接:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");

在以上代碼執行之前,DriverManager會先被類加載器加載,因爲java.sql.DriverManager類是位於rt.jar下面的 ,所以他會被根加載器加載。

類加載時,會執行DriverManager類的靜態方法。其中有一段關鍵的代碼是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

這段代碼,會嘗試加載classpath下面的所有實現了Driver接口的實現類。

那麼,問題就來了。

DriverManager是被根加載器加載的,那麼在加載時遇到以上代碼,會嘗試加載所有Driver的實現類,但是這些實現類基本都是第三方提供的,根據雙親委派原則,第三方的類不能被根加載器加載。

那麼,怎麼解決這個問題呢?

於是,就在JDBC中通過引入Thread ContextClassLoader(線程上下文加載器,默認情況下是AppClassLoader)的方式破壞了雙親委派原則。

Thread ContextClassLoader 線程上下文類加載器

這個ClassLoader可以通過 java.lang.Thread類的setContextClassLoaser()方法進行設置;如果創建線程時沒有設置,則它會從父線程中繼承;如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認爲AppClassLoader

public class Thread implements Runnable {
    // 這裏省略了無關代碼
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 這裏省略了無關代碼
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader; // 繼承父線程的 上下文類加載器       
        // 這裏省略了無關代碼       
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    // 這裏省略了無關代碼     
    
    public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }
}

有了Thread ContextClassLoader,就可以實現父ClassLoader讓子ClassLoader去完成一個類的加載任務,即父ClassLoader加載的類中,可以使用ContextClassLoader去加載其無法加載的類)。

ServiceLoader load

DriverManager類在被加載的時候就會執行通過ServiceLoader#load方法來加載數據庫驅動(即Driver接口的實現)。

簡單考慮以上代碼的類加載過程爲:可以想一下,DriverManager類由BootstrapClassLoader加載,DriverManager類依賴於ServiceLoader類,因此BootstrapClassLoader也會嘗試加載ServiceLoader類,這是沒有問題的;

再往下,ServiceLoaderload方法中需要加載數據庫(MySQL等)驅動包中Driver接口的實現類,即ServiceLoader類依賴這些驅動包中的類,此時如果是默認情況下,則還是由BootstrapClassLoader來加載這些類,但驅動包中的Driver接口的實現類是位於CLASSPATH下的,BootstrapClassLoader是無法加載的。

ServiceLoader#load方法中實際是指明瞭由Thread ContextClassLoader來加載驅動包中的類:

public final class ServiceLoader<S> implements Iterable<S> {
    // 省略無關代碼
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 需要注意的是,這裏使用的是 當前線程的 ContextClassLoader 來加載實現,這也是 ContextClassLoader 爲什麼存在的原因。
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }  
} 

參考:

我竟然被”雙親委派”給虐了!

理解 Java Thread ContextClassLoader(線程上下文類加載器)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章