Java類加載機制-筆記4(雙親委派機制)

雙親委派機制

需求: 在默認情況下,一個限定名的類只會被一個類加載器加載并解析使用,這樣在程序中,他就是不唯一的,不會產(chǎn)生歧義。

如何實現(xiàn)這種需求?
JVM的開發(fā)者引入了雙親委派模型,這個名字聽上去很高大上,其實邏輯非常簡單,我們通過這張圖來理解一下:

雙親委派模型

解釋一下這張圖,也就是說:在被動的情況下,當一個類收到加載請求,他不會首先自己去加載,而是傳遞給自己的父親加載器,這樣所有的類都會傳遞到最上層的Bootstrap ClassLoader ,只有父親加載器無法完成加載,那么此時兒子加載器才會自己去嘗試加載,什么叫無法加載?就是根據(jù)類的限定名類加載器沒有在自己負責的加載路徑中找到該類,這里注意:父親加載器、兒子加載器,不同于父加載器,子加載器,因為上圖中這些箭頭并不表示繼承關(guān)系,而是一種邏輯關(guān)系,實際上是通過組合的方式來實現(xiàn)的,這也是很多博客上沒有寫清楚的容易誤導人的一點。接下來我們就通過源碼來看下雙親委派機制具體是怎么實現(xiàn)的。

代碼很簡單(取自java.lang.ClassLoader):

    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 {
                       // parent == null 代表 parent為bootstrap classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 說明parent加載不了,當前l(fā)oader嘗試 findclass
                    // 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;
        }
    }

首先檢查該類是否已經(jīng)被加載過,如果沒有,則開啟加載流程,如果有,則直接讀取緩存。parent變量代表了當前classloader的父親加載器,這里就體現(xiàn)了,不是通過繼承而是通過組合的方式實現(xiàn)類加載器之間的 父子關(guān)系。如果parent==null,約定parent是bootstrap classloader ,因為最開始我們也說過,bootstrap classloader 是由JVM內(nèi)部實現(xiàn)的,沒有辦法被程序引用,所以這里就約定為null,當parent為null,就調(diào)用findBootstrapClassOrNull這個方法,讓bootstrap classloader 嘗試進行加載,如果parent不為null,那么就讓parent根據(jù)限定名去嘗試加載該類,并返回class對象。如果返回的class對象為null,那么就說明parent沒有能力去加載這個類,那么就調(diào)用findClass,findClass表示如何去尋找該限定名的class需要各個類加載器自己實現(xiàn),比如Extension ClassLoader 和Application ClassLoader都使用了這段邏輯來實現(xiàn)自己的findClass。
(取自java.net.URLClassLoader)

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

這里可以看到,通過將類的限定名轉(zhuǎn)化為文件path,再通過ucp這個對象去進行尋找,找到文件資源后,再調(diào)用defineClass去進行類加載的后續(xù)流程,
defineClass 方法(java.net.URLClassLoader)

    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        int len = b.remaining();

        // Use byte[] if not a direct ByteBufer:
        if (!b.isDirect()) {
            if (b.hasArray()) {
                return defineClass(name, b.array(),
                                   b.position() + b.arrayOffset(), len,
                                   protectionDomain);
            } else {
                // no array, or read-only array
                byte[] tb = new byte[len];
                b.get(tb);  // get bytes out of byte buffer.
                return defineClass(name, tb, 0, len, protectionDomain);
            }
        }

        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

defineClass 方法是由java.lang.ClassLoader中一個被final修飾的方法,意味著獲取到class二進制流以后呢,最終將會由java.lang.classloader 來進行后續(xù)的操作,因為它是被final修飾的,即不允許被外部重寫,這符合了我們最開始所說的類加載過程中除了讀取二進制流的操作外剩余邏輯都是有JVM內(nèi)部實現(xiàn)的設(shè)計,這就是雙親委派模型。
我們在看一下上回提到的兩個問題:

問題:
1.不同的類加載器,除了讀取二進制流的動作和范圍不一樣,后續(xù)的加載器邏輯是否也不一樣?
2.遇到限定名一樣的類,這么多類加載器會不會產(chǎn)生混亂?

解答:
1.我們認為除了Bootstrap ClassLoader,所有的非Bootstrap ClassLoader都繼承了java.lang.ClassLoader,都由這個類的defineClass進行后續(xù)處理。
2.越核心的類庫越被上層的類加載器加載,而某限定名的類一旦被加載過了,被動情況下,就不會再加載相同限定名的類。這樣,就能夠有效避免混亂。

破壞雙親委派

第一次破壞雙親委派

但是雙親委派模型,并不是一個具有強約束力的模型。因為它存在設(shè)計缺陷,在大部分被動情況下,也就是上層開發(fā)者正常寫代碼,沒有騷操作的情況下,他是生效并且好用的。在一些情況下,雙親委派模型可以被主動破壞,細心的同學可能已經(jīng)發(fā)現(xiàn)了,我上面自己寫的用于被證明類加載器存在命名空間的demo就是一次對雙親委派模型的破壞,可以看到,這里自定義的類加載器直接重寫了java.lang.ClassLoader的loadClass方法,而雙親委派的邏輯就是存在于這個方法內(nèi)的,那么我的這個重寫就代表了對原有雙親委派邏輯的破壞,所以就出現(xiàn)了一個限定名對應(yīng)兩種不同class的情況,
需要提出的 是,除非是有特殊的業(yè)務(wù)場景,一般來說不要去主動破壞雙親委派模型,那么JVM推薦并希望開發(fā)者遵循雙親委派模型,那么為什么不把loadClass方法像defineClass方法一樣設(shè)定成final來修飾?那這樣的情況,就沒有辦法去重寫loadClass方法,也就代表著上層開發(fā)者盡量遵循雙親委派的邏輯了。
因為這是JVM開發(fā)者必須面對,但是無法解決的問題,java.lang.ClassLoader 的loadClass方法,在java很早的版本就有了,而雙親委派模型是在JDK1.2引入的特性,Java是向下兼容的,也就是說,引入雙親委派機制時,世界上已經(jīng)存在了很多像上面一樣的代碼。JVM既然無法拒絕支持,只能默默接受,一點補救措施呢,就是在JDK1.2版本后引入了findClass方法,推薦用戶去重寫該方法而不是直接重寫loadClass方法,這樣就毅然能符合雙親委派,這是史上第一次破壞雙親委派。

第二次破壞雙親委派

我們舉個例子:比如JDK想要提供操作數(shù)據(jù)庫的功能。
那么數(shù)據(jù)庫有很多種,并且隨著時間的推移,將會出現(xiàn)更多的品種的數(shù)據(jù)庫,比較合理的方式是,JDK提供一組規(guī)范、一組接口,各個不同的數(shù)據(jù)庫廠商按照這個接口去自己實現(xiàn)自己類庫。
這里就問題就出現(xiàn)了:
對JDK代碼包中的加載肯定使用了上層的類加載器,比如說bootstrapClassLoader 但當你去調(diào)用JDK 中的接口時,接口所在的類將會引起第三方類庫的加載這就不符合自下而上的委派加載順序了,而是出現(xiàn)了上層類加載器放下身段去調(diào)用下層類加載器的情況,這就產(chǎn)生了對雙親委派模型的破壞。

這就是Java的SPI
我們可以把SPI理解成一種服務(wù)發(fā)現(xiàn)機制,各大廠商的服務(wù)注冊到JDK提供的接口上,上層在調(diào)用JDK的接口時,JDBC是SPI的其中一種功能,在上面的例子中我們在JDBC上注冊了mysql Driver,h2 Driver這兩種服務(wù),那么這里SPI究竟是如何對雙親委派進行破壞的呢,我們看一下DriverManager的源碼來簡單看一下:
可以看到DriverManager會主動的對第三方Driver進行加載,掃描到所有注冊為java.sql.driver類型的第三方類就使用serviceLoader去進行加載,而serviceLoader內(nèi)部使用了當前線程context中的類加載器,一般線程context中的類加載器默認為application ClassLoader ,所以這些第三方類也就能夠被正常加載了,所以再結(jié)合這些輸出內(nèi)容。


第三次破壞雙親委派

隨著人們對模塊化的追求,希望在程序運行時,能夠動態(tài)的對部分組件代碼進行替換,這就是所謂的熱替換、熱部署,想想也能夠大致猜到,這里又將會出現(xiàn)很多的自由的類加載操作,所以又將是一次對雙親委派模型的踐踏。

問題: 能不能自己寫一個限定名為java.lang.String的類,并在程序中調(diào)用它?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容