Java類加載機制:雙親委派機制,還是應該叫做“父委派模型”?

閱讀這篇文章,你會瞭解到:
1.上面是類加載器
2.爲什麼應該叫做“父委派模型”,而不是“雙親委派機制”
3.在JNDI中,“父委派模型”是怎麼被違背的
4.不只是JNDI,還有TOMCAT的類加載器模型是怎樣的,他們有無違背“父委派模型”?

一.什麼是類加載器

講“雙親委派機制”前,要先要講一講類和類加載器的關係

1.類(Class)

我們在編寫代碼時,創建的每個“*.java”文件都可以認爲是一個類,我們使用“class”去定義一個類,例如String.java。

2.類加載器(Class Loader)

(1)我們定義的類,如果我們要在編碼中用到這個類,首先就是要先把“*.java”這個文件編譯成class文件,然後由對應的“類加載器”加載到JVM中,我們才能夠使用這個“類對象”。
(2)一般的場景下,類的加載是在我們程序啓動的時候由jvm來完成,但是有些場景可能需要我們手動去指定加載某個類或找到某個類,這時候就要用到 Class.forName(String className) 加載/找到 這個className對應的類。
(3)如果比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來自同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這個兩個類就必定不相等。

在我們日常使用中,類加載器默認有下面3種:
(1)Bootstrap Class Loader:
JDK自帶的一款類加載器,用於加載JDK內部的類。Bootstrap類加載器用於加載JDK中$JAVA_HOME/jre/lib下面的那些類,比如rt.jar包裏面的類。
(2)Extension Class Loader
主要用於加載JDK擴展包裏的類。一般$JAVA_HOME/lib/ext下面的包都是通過這個類加載器加載的,這個包下面的類基本上是以javax開頭的。
(3)Application Class Loader
用來加載開發人員自己平時寫的應用代碼的類的,加載存放在classpath路徑下的那些應用程序級別的類的。

二.爲什麼應該叫做“父委派模型”,而不是“雙親委派機制”?

這是個很蛋疼的翻譯問題,實際上在oracle官方文檔上,人家是這樣描述的:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

java平臺通過委派模型去加載類。每個類加載器都有一個父加載器。當需要加載類時,會優先委派當前所在的類的加載器的父加載器去加載這個類。如果父加載器無法加載到這個類時,再嘗試在當前所在的類的加載器中加載這個類。

所以,java的類加載機制應該叫做“父委派模型”,不應該叫做“雙親委派機制”,“雙親委派機制”這個名字太具有誤導性了。

三.“父委派模型”是怎麼工作的?

舉個例子,當前有個Test.class,需要加載rt.jar中的java.lang.String,那麼加載的流程如下圖所示,整體的加載流程是向上委託父加載器完成的。

如果整個鏈路中,父加載器都沒有加載這個類,且無法加載這個類時,纔會由Test.class所在的加載器去加載某個類(例如希望加載開發人員自定義的類 Test2.class)。

四.“父委派模型”有什麼好處?

“父委派模型”保證了系統級別的類的安全性,使一些基礎類不會受到開發人員“定製化”的破壞。

如果沒有使用父委派模型,而是由各個類加載器自行加載的話,如果開發人員自己編寫了一個稱爲java.lang.String的類,並放在程序的ClassPath中,那系統將會出現多個不同的String類, Java類型體系中最基礎的行爲就無法保證。應用程序也將會變得一片混亂。

五. “父委派模型”什麼時候會遭到破壞?

  • 通過預加載的方式;
  • 通過Thread.getContextClassLoader();

1.通過預加載的方式

這裏通過一個簡單的例子,就拿sql連接來說:

(1)java.sql.DriverManager:rt.jar包中的類,通過Bootstrap加載器加載。
(2)DriverTest:開發人員自定義的實現了java.sql.Driver接口的類型,通過App加載器加載。

開發人員通過DriverManager.registerDriver方法把自己實現的獲取連接的Driver實現類加載並註冊到DriverManager中。然後DriverManager.getConnection方法會遍歷所有註冊的Driver,並觸發Driver的connect接口來獲取連接。(即繞過在DriverManager所在的Bootstrap加載器,因爲Bootstrap加載器不能加載開發人員實現的Driver類)

定義一個 DriverTest 類,實現rt.jar裏面的java.sql.Driver接口

public class DriverTest implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new DriverTest());
            System.out.println("who load DriverTest: " + DriverTest.class.getClassLoader());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        return new Connection() {
        //此處省略一堆代碼......
        }
    }
    
	//啓動代碼
    public static void main(String[] args) {
        try {
			//由AppClassLoader加載DriverTest類
			Class.forName("com.jenson.pratice.classloader.DriverTest");
            System.out.println("who load DriverManager: "+DriverManager.class.getClassLoader());
            //通過rt.jar中的DriverManager去獲取鏈接,DriverManager由BootstrapClassLoader加載
            Connection connection = DriverManager.getConnection("jdbc://");

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

此時運行main方法打印:

who load DriverTest: sun.misc.Launcher$AppClassLoader@18b4aac2
who load DriverManager: null
Process finished with exit code 0
---------------------
DriverManager是由Bootstrap加載器的,因而獲取不了Bootstrap加載器,所以爲null。從父委派模型的機制上看,因爲rt.jar是由Bootstrap加載器加載的,所以裏面的類,都不能用到rt.jar以外的類。

那麼DriverManager.getConnection是怎麼調用DriverTest(App加載器)的getConnection方法呢?

因爲父委派模型的限制,DriverManager不可能自己去加載DriverTest,DriverTest的加載實際上是由AppClassLoader完成的,DriverTest裏面會往
DriverManager中註冊一個驅動。

public class DriverTest implements java.sql.Driver {
    static {
        try {
        	//在這裏註冊
            java.sql.DriverManager.registerDriver(new DriverTest());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

對於DriverManager而言,他不關注driver的加載,他只需要遍歷“registeredDrivers”,然後檢查驅動類是否能被“調用類的類加載器”識別,如果可以識別,則調用driver.connect方法(即DriverTest中的實現)

 public class DriverManager{
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //省略一堆代碼
        for(DriverInfo aDriver : registeredDrivers) {
        //在這裏做安全校驗
if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //在這裏調用DriverTest的connect方法
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
                //省略一堆代碼
                

整體的流程是這樣的

所以可以看到,在DriverManager中要調用DriverTest的方法,並沒有通過“父委派模型”去加載DriverTest,而是由下層的類加載器自行完成類的加載。這裏實際上是繞過了“父委派模型”的機制。

2. 通過Thread.getContextClassLoader

Thread類中有一個contextClassLoader屬性,稱爲上下文類加載器。在實例化一個線程時,如果沒有設置contextClassLoader屬性,默認會從父線程中繼承。如果在應用程序的全局範圍內都沒有設置過多的話,默認爲Application Class Loader。

舉個例子:rt.jar中的 javax.xml.parsers.FactoryFinder 中的 newInstance方法:
(1)在newInstance中會用到 getProviderClass 方法
(2)在getProviderClass中會用到 SecuritySupport.getContextClassLoader方法
(3)在SecuritySupport.getContextClassLoader中會用到Thread.currentThread().getContextClassLoader()拿到線程上下文類加載器

   /**
     * Create an instance of a class. Delegates to method
     * <code>getProviderClass()</code> in order to load the class.
     * 
     * 定義的JNDI接口
     * @param type Base class / Service interface  of the factory to instantiate
     * JNDI的實現類名
     * @param className Name of the concrete class corresponding to the service provider
     * 加載器:如果爲null,則通過線程的上下文加載器進行加載
     * @param cl <code>ClassLoader</code> used to load the factory class. If <code>null</code>
     * current <code>Thread</code>'s context classLoader is used to load the factory class.
     * 如果爲true,則使用bootstrap加載器。
     * @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
     * is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
     */
    static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
                             boolean doFallback, boolean useBSClsLoader)
        throws FactoryConfigurationError
    {
        //省略一堆代碼
        try {
        	//在這個方法裏面,可以通過線程上下文加載器進行加載className對應的類
            Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
            //省略一堆代碼
    static private Class<?> getProviderClass(String className, ClassLoader cl,
            boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
    {
        try {
            if (cl == null) {
                if (useBSClsLoader) {
                    return Class.forName(className, false, FactoryFinder.class.getClassLoader());
                } else {
                    //在這裏,會獲得線程的上下文加載器去加載類
                    //其中 ss是 SecuritySupport.java
                    cl = ss.getContextClassLoader();
                    if (cl == null) {
                        throw new ClassNotFoundException();
                    }
                    else {
                        return Class.forName(className, false, cl);
                    }
                }
            }
            //省略一堆代碼
    }
class SecuritySupport  {
    ClassLoader getContextClassLoader() throws SecurityException{
        return (ClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                ClassLoader cl = null;
                //try {
                //獲得線程的上下文加載器
                cl = Thread.currentThread().getContextClassLoader();
                //} catch (SecurityException ex) { }

                if (cl == null)
                    cl = ClassLoader.getSystemClassLoader();

                return cl;
            }
        });
    }

六.關於tomcat的類加載機制

不只是Driver驅動的實現是這樣,在tomcat、spring等等的容器框架也是通過一些手段去繞過“父委派機制”。

例如下圖中的tomat類加載器的結構:

從圖中的委派關係中可以看出:

  1. CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實現了公有類庫的共用。
  2. CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。
  3. WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。
  4. JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。

那麼tomcat 違背了父委派模型嗎?

tomcat 違背了父委派模型。因爲雙親委派模型要求除了頂層的啓動類加載器之外,其餘的類加載器都應當由自己的父類加載器加載。
而tomcat 不是這樣實現,tomcat 爲了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。

七.參考文檔

https://www.cnblogs.com/tiancai/p/9317299.html
https://blog.csdn.net/qq_38182963/article/details/78660779
https://www.cnblogs.com/doit8791/p/5820037.html
https://blog.csdn.net/lengxiao1993/article/details/86689331

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