一篇圖文徹底弄懂類加載器與雙親委派機(jī)制

無論你是跟同事、同學(xué)、上下級、同行、或者面試官討論技術(shù)問題的時(shí)候,很容易卷入JVM大型撕逼現(xiàn)場。為了能夠讓大家從大型撕逼現(xiàn)場中脫穎而出,最近我苦思冥想如何把知識點(diǎn)盡可能呈現(xiàn)的容易理解,方便記憶。于是就開啟了這一系列文章的編寫。為了讓JVM相關(guān)知識點(diǎn)能夠形成一個(gè)體系,arthinking將編寫整理一系列的專題,以盡量以圖片的方式描述相關(guān)知識點(diǎn),并且最終把所有相關(guān)知識點(diǎn)串成了一張圖。持續(xù)更新中,歡迎大家閱讀。有任何錯(cuò)落之處也請您高抬貴手幫忙指正,感謝!

導(dǎo)讀:

  1. 類加載器是怎么被創(chuàng)建出來的?
  2. 什么是雙親委派機(jī)制?為什么要有這種機(jī)制?
  3. Class實(shí)例和類加載器究竟是在Java Heap中,還是在方法區(qū)中?

類加載器: 可以實(shí)現(xiàn)通過一個(gè)類的全限定名稱來獲取描述此類的二進(jìn)制字節(jié)流。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊成為”類加載器“。

通過自定義類加載器可以實(shí)現(xiàn)各種有趣而強(qiáng)大的功能更:OSGi,熱部署,代碼加密等。

1、類加載器的加載流程

image-20200105144705999

如上圖為類加載器的加載流程。

這里簡單描述下:

1.1、啟動(dòng)類加載器

啟動(dòng)類加載器:系統(tǒng)啟動(dòng)的時(shí)候,首先會(huì)通過由C++實(shí)現(xiàn)的啟動(dòng)類加載器,加載<JAVA_HOME>/lib目錄下面的jar包,或者被-Xbootclasspath參數(shù)指定的路徑并且被虛擬機(jī)識別的文件名的jar包。把相關(guān)Class加載到方法區(qū)中。

這一步會(huì)加載關(guān)鍵的一個(gè)類:sun.misc.Launcher。這個(gè)類包含了兩個(gè)靜態(tài)內(nèi)部類:

  • ExtClassLoader:擴(kuò)展類加載器內(nèi)部類,下面會(huì)講;
  • AppClassLoader:應(yīng)用程序類加載器內(nèi)部類,下面會(huì)講

可以反編譯rt.jar文件查看詳細(xì)代碼:

image-20200105124613663
image-20200105131342939

在加載到Launcher類完成后,會(huì)對該類進(jìn)行初始化,初始化的過程中,會(huì)創(chuàng)建 ExtClassLoader 和 AppClassLoader,源碼如下:

public Launcher() {
    ExtClassLoader extClassLoader;
    try {
      extClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException iOException) {
      throw new InternalError("Could not create extension class loader", iOException);
    }
    try {
      this.loader = AppClassLoader.getAppClassLoader(extClassLoader);
    } catch (IOException iOException) {
      throw new InternalError("Could not create application class loader", iOException);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...

由于啟動(dòng)類加載器是由C++實(shí)現(xiàn)的,所以在Java代碼里面是訪問不到啟動(dòng)類加載器的,如果嘗試通過String.class.getClassLoader()獲取啟動(dòng)類的引用,會(huì)返回null

問題:

  1. 啟動(dòng)類加載器,擴(kuò)展類加載器和應(yīng)用類加載器都是又誰加載的?

    1. 啟動(dòng)類加載器是JVM的內(nèi)部實(shí)現(xiàn),在JVM申請好內(nèi)存之后,由JVM創(chuàng)建這個(gè)啟動(dòng)類加載器
    2. 擴(kuò)展類加載器和應(yīng)用程序類加載器是由啟動(dòng)類加載器加載進(jìn)來的;
  2. 說說以下代碼輸出什么:

 public static void main(String[] args) {
     System.out.println("加載當(dāng)前類的加載器:" + TestClassLoader.class.getClassLoader());
        System.out.println("加載應(yīng)用程序類加載器的加載器"
                         + TestClassLoader.class.getClassLoader().getClass().getClassLoader());
        System.out.println("String類的啟動(dòng)類加載器" + String.class.getClassLoader());
   }

1.2、擴(kuò)展類加載器

如上圖,擴(kuò)展類加載器負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者被java.ext.dirs系統(tǒng)變量指定的路徑中的類。

1.3、應(yīng)用程序類加載器

引用程序類加載器加載用戶類路徑下制定的類庫,如果應(yīng)用程序沒有自定義過自己的類加載器,此類加載器就是默認(rèn)的類加載器。

引用程序類加載器也叫系統(tǒng)類加載器,可以通過getSystemClassLoader方法得到應(yīng)用程序類加載器。

注意,如上圖通過以上三個(gè)類加載器加載類到方法區(qū)之后,方法區(qū)中分別對應(yīng)有各自的類信息存儲(chǔ)區(qū)。不同類加載器加載的同一個(gè)類文件不相等。

2、類加載器的雙親委派機(jī)制

2.1、雙親委派機(jī)制原理

雙親委派模型在JDK1.2之后被引入,并廣泛使用,這不是一個(gè)強(qiáng)制性的約束模型,二貨思Java設(shè)計(jì)者推薦給開發(fā)者的一種類加載器實(shí)現(xiàn)方式。我們也可以覆蓋對應(yīng)的方式,實(shí)現(xiàn)自己的加載模型。

類加載器的雙親委派機(jī)制如下:

image-20200105170731274

也就是說:

  • 一個(gè)類加載器收到了類加載請求,不會(huì)自己立刻嘗試加載類,而是把請求委托給父加載器去完成,每一層都是如此,所有的家在請求最終都傳遞到最頂層的類加載器進(jìn)行處理;
  • 如果父加載器不存在了,那么嘗試判斷有沒有被啟動(dòng)類加載器加載;
  • 如果的確沒有被夾在,則再自己嘗試加載。

問題:

  1. 為什么要有這么復(fù)雜的雙親委派機(jī)制?
    1. 如果沒有這種機(jī)制,我們就可以篡改啟動(dòng)類加載器中需要的類了,如,修自己編寫一個(gè)java.lang.Object用自己的類加載器進(jìn)行加載,系統(tǒng)中就會(huì)存在多個(gè)Object類,這樣Java類型體系最基本的行為也就無法保證了。

2.2、雙親委派機(jī)制處理流程

JDK中默認(rèn)的雙親委派處理流程是怎么的呢?接下來我們看看代碼,以下是java.lang.ClassLoader.loadClass()方法的實(shí)現(xiàn):

    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;
        }
    }

轉(zhuǎn)成流程圖,即是:

image-20200105174045231

如山圖所以,總是先回嘗試讓父類加載器先加載,其次判斷啟動(dòng)類加載器是否已經(jīng)加載了,最后才嘗試從當(dāng)前類加載器加載。轉(zhuǎn)換為更清晰的模型如下:

image-20200105195158889

雙親委派模型具有以下特點(diǎn):

  • 可見性原則:
    • 應(yīng)用類加載器是可以讀取到由擴(kuò)展類加載器和啟動(dòng)類加載器加載進(jìn)來的Class的;
    • 擴(kuò)展類加載器是可以讀取到由啟動(dòng)類加載器加載進(jìn)來的Class的;
  • 唯一性:
    • 類是唯一的,沒有重復(fù)的類;

2.3、類加載器和Class實(shí)例的題外話

啟動(dòng)類加載器,擴(kuò)展類加載器,應(yīng)用程序類加載器,他們分別管理者各自方法區(qū)里的一個(gè)區(qū)塊。

根據(jù)上一篇文章我們知道,方法區(qū)里面主要存儲(chǔ)的是類的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),這個(gè)類的在方法區(qū)中的各種數(shù)據(jù)結(jié)構(gòu)信息通過類的Class實(shí)例進(jìn)行訪問。

如下圖:

image-20200105200625589

方法區(qū)里面存儲(chǔ)著加載進(jìn)來的類信息,方法區(qū)同時(shí)雇傭了兩類工種幫忙干活:

  • 類加載器:負(fù)責(zé)管理各個(gè)存儲(chǔ)區(qū)的類信息,如加載和卸載類信息;
  • Class實(shí)例:負(fù)責(zé)對接外部需求,如果外部有人想查看里面的類信息,則需要通過Class實(shí)例來獲取;

另外,方法區(qū)里面,啟動(dòng)類加載器類信息對擴(kuò)展兩類加載器類信息可見,而前面兩者的類信息又對應(yīng)用程序類加載器類信息可見。

3、其他非雙親委派模型的案例

3.1、JDK 1.0遺留問題

在JDK1.0已經(jīng)存在了ClassLoader類,但是當(dāng)時(shí)還沒有雙親委派機(jī)制,用戶為了自定義類加載器,需要重新loadClass()方法,而我們知道,在JDK1.2以后,loadClass里面就是雙親委派機(jī)制的實(shí)現(xiàn)代碼,此時(shí),要實(shí)現(xiàn)自定義類加載器,需要重新findClass()類即可。

如果重新了loadClass()方法,也就意味著不再遵循雙親委派模型了。

3.2、線程上下文類加載器

為什么需要這個(gè)東西呢,我們還是從一個(gè)案例來說起。

Tomcat中的類加載器

我們知道Tomcat目錄結(jié)構(gòu)中有以下目錄:

  • /common/: 該目錄下的類庫可被Tomcat和所有的WebApp共同使用;

  • /server/: 該目錄下的類庫可被Tomcat使用,但對所有的WebApp不可見;

  • /shared/: 該目錄下的類庫可被所有的WebApp共同使用,但對Tomcat自己不可見;

另外Web應(yīng)用程序還有自身的類庫,放在/WebApp/WEB-INF目錄中:這里面的類庫僅僅可以被此Web應(yīng)用程序使用,對Tomcat和其他Web應(yīng)用程序都不可見。
為了實(shí)現(xiàn)以上各個(gè)目錄的類庫可見性效果,Tomat提供了如下的自定義類加載器:

image-20200105205509075

現(xiàn)在如下場景:

我們發(fā)現(xiàn)Tomcat下面有若干個(gè)webapp,每個(gè)webapp都用到了spring,于是我們把spring的jar包放到了shared目錄中。

于是問題出現(xiàn)了:由于spring的jar包是由Shared類加載器加載的,假設(shè)我們要使用SpringContext的getBean方法,獲取webapp中的Bean,如果是按照雙親委派模型,就會(huì)有問題了,因?yàn)閣ebapp中的Java類是對SharedClassLoader不可見的:

image-20200105213630571

Spring中的線程上下文類加載器

為了解決這個(gè)問題,Spring使用了線程上下文類加載器,即從ThreadLocal中獲取到當(dāng)前線程的上下文類加載器,來加載所有的類庫和類。

關(guān)于Spring初始化源碼相關(guān)解讀,參考我的這邊文章:Spring IoC原理剖析

Spring中的bean類加載器

ApplicationContext中有一個(gè)beanClassLoader字段,這個(gè)是bean的類加載器,在prepareBeanFactory()方法中做了初始化:

beanFactory.setBeanClassLoader(getClassLoader());

getClassLoader方法如下:

    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

ClassUtils.getDefaultClassLoader()方法:

    @Nullable
    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        }
        catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

可以發(fā)現(xiàn),這里最終取了當(dāng)前線程上下文中的ClassLoader。

加載Bean

我們來看看Spring加載Class的代碼。這里我們直接找到實(shí)例化Singletons的方法跟進(jìn)去找需要關(guān)注的代碼:

我們發(fā)現(xiàn)在加載Bean Class的時(shí)候調(diào)用了這個(gè)方法:

AbstractBeanFactory:

ClassLoader beanClassLoader = getBeanClassLoader();

也就是用到了ApplicationContext中的beanClassLoader,線程上下文類加載器來加載Bean Class實(shí)例。

總結(jié)

Spring作為一個(gè)第三方類庫,可能被任何的ClassLoader加載,所以最靈活的方式是直接使用上下文類加載器。

3.3、模塊熱部署

主要是類似OSGi這類的模塊化熱部署技術(shù)。在OSGi中不再是雙親委派模型中的樹狀結(jié)構(gòu),而是更復(fù)雜的網(wǎng)狀結(jié)構(gòu)。

References

Where are static methods and static variables stored in Java?

ClassLoader in Java

真正理解線程上下文類加載器(多案例分析)

《深入理解Java虛擬機(jī)-JVM高級特性與最佳實(shí)踐》

Chapter 5. Loading, Linking, and Initializing


本文為arthinking基于相關(guān)技術(shù)資料和官方文檔撰寫而成,確保內(nèi)容的準(zhǔn)確性,如果你發(fā)現(xiàn)了有何錯(cuò)漏之處,煩請高抬貴手幫忙指正,萬分感激。

大家可以關(guān)注我的博客:itzhai.com 獲取更多文章,我將持續(xù)更新后端相關(guān)技術(shù),涉及JVM、Java基礎(chǔ)、架構(gòu)設(shè)計(jì)、網(wǎng)絡(luò)編程、數(shù)據(jù)結(jié)構(gòu)、數(shù)據(jù)庫、算法、并發(fā)編程、分布式系統(tǒng)等相關(guān)內(nèi)容。

如果您覺得讀完本文有所收獲的話,可以關(guān)注我的賬號,或者點(diǎn)贊的,您的支持就是我寫作的動(dòng)力!關(guān)注我的公眾號,及時(shí)獲取最新的文章。


本文作者: arthinking

博客鏈接: https://www.itzhai.com/jvm/what-is-classloader-and-what-is-parents-delegation-model.html

一篇圖文徹底弄懂類加載器與雙親委派機(jī)制

版權(quán)聲明: 版權(quán)歸作者所有,未經(jīng)許可不得轉(zhuǎn)載,侵權(quán)必究!聯(lián)系作者請加公眾號。


image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評論 2 373