Java類加載機制-雙親委派

Java類加載

? ? ? ?在Java中,一個類如果要想正確運行,就必須通過JVM編譯幕垦,然后將其載入內存中才能使用,這里的載入內存中傅联,實際上就是:類在JVM中以java.lang.Class類型的對象存在薄风。說到Class類型瘫俊,了解反射的都比較清楚,這是反射中常用的一個類型,獲取Class對象一般使用兩種方式:通過類的具體對象的getClass方法(obj.getClass())划纽、通過類的class屬性獲取(Object.class)痊臭。

? ? ? ?Java中的類加載實際上都是通過字節(jié)流來來實現(xiàn)的,字節(jié)流的來源可以有多種方式,可以從文件中獲取岛抄,也可以通過網絡獲取,雖然來源不同狈茉,但是只要最終是字節(jié)流形式夫椭,就可以通過JVM來解析并加載。在此先說明一下:類在加載過程中都經歷了那些步驟氯庆。

類的生命周期

? ? ? ?類從被虛擬機加載蹭秋,直到最后被卸載出內存,這一整個過程成為類的生命周期点晴,大致可以分為七個階段:

類的生命周期.png

? ? ? ?這里需要注意:除去解析這個步驟感凤,其余步驟的順序都是確定的,解析階段規(guī)則有點不同粒督,在有些情況下陪竿,解析可以在初始化之后再進行,這也是Java語言的運行時綁定的一個基礎保障屠橄。

? ? ? ?這里先說說初始化族跛,在虛擬機規(guī)范中,嚴格規(guī)定了初始化所必須具備的條件锐墙,即:如果滿足必備條件礁哄,將必須執(zhí)行初始化操作。共有5中情況(能夠到初始化這一步溪北,也就說明前面都已經驗證通過了):

  • 遇到new桐绒、getstatic、putstatic或invokestatic這四條指令代碼之拨,如果對應類還沒有初始化茉继,則必須進行初始化,對應與Java中常用的場景就是:實例化一個對象(new)蚀乔、讀取或設置一個靜態(tài)字段(getstatic烁竭、putstatic;這里需要額外說明:final修飾的常量不包括在內吉挣,因為它屬于在編譯器就已經把它放到了靜態(tài)常量池中了)派撕、調用一個方法的時候(invokestatic)。

  • 在使用reflect包下的方法時睬魂,對類進行反射調用的時候终吼,如果沒有初始化,先觸發(fā)其初始化

  • 當初始化某一個類的時候氯哮,它的父類還沒有進行初始化衔峰,那么就先初始化它的父類

  • 虛擬機啟動時,我們指定的那個包含main方法的類(執(zhí)行主類)會先被初始化

  • JDK1.7的動態(tài)語言支持部分,在使用java.lang.invoke.MethodHandler實例解析最后結果中的方法句柄垫卤,如果方法句柄所對應的類沒有被初始化,則需要先觸發(fā)其初始化出牧。

類加載

? ? ? ?類的加載過程可以分為加載穴肘、驗證、準備舔痕、解析和初始化這五個步驟评抚。也就是上圖中除去使用和卸載兩個步驟之外的過程。

加載

? ? ? ?加載過程是類加載過程的一個部分伯复,換句話說:ClassLoading過程包括加載過程慨代,但不僅僅只有加載過程,在加載階段啸如,虛擬機需要完成3個步驟:

  • 通過一個類的全限定名來獲取此類的二進制字節(jié)流侍匙。

  • 將此字節(jié)流所代表的靜態(tài)存儲結構轉換成方法區(qū)中的運行時數(shù)據(jù)結構

  • 在內存中生成一個Class對象,在方法區(qū)中作為這個類訪問入口叮雳。

? ? ? ?通過第一步可以知道想暗,這里并沒有準確定義二進制流具體要從哪里獲取,怎樣獲取帘不。因此可以發(fā)揮的空間就比較大了说莫,例如:可以通過壓縮包中讀取(zip寞焙、jar储狭、war等等)、可以從網絡中獲取捣郊、運行時動態(tài)計算(動態(tài)代理)辽狈、其他文件生成(JSP)等等,方法多樣模她,可以根據(jù)具體應用場景來自由選擇稻艰。

驗證

? ? ? ?這一步是至關重要的一部,目的就是為了確保二進制字節(jié)流中包含的信息是不是與當前虛擬機的要求相符合侈净,同時會不會對虛擬機有危險尊勿。驗證階段的嚴謹性直接決定了虛擬機的強壯性,嚴謹?shù)尿炞C過程可以保證虛擬機能夠抵御各種惡意代碼的攻擊畜侦。這一過程如果不通過元扔,會拋出java.lang.VerifyError。主要分為四個步驟:

  • 文件格式驗證:就是驗證字節(jié)流是否符合規(guī)范旋膳,如:是否以魔數(shù)0xCAFEBABE開頭澎语、主次版本號是否在虛擬機的處理范圍之內...等等

  • 元數(shù)據(jù)驗證:這段主要是對字節(jié)流中的信息進行語義分析,保證符合Java的語言規(guī)范,如:驗證是否有父類(除Object之外擅羞,所有類都應當有父類)尸变、是否存在繼承了final修飾的類...等等

  • 字節(jié)碼驗證:這塊比較復雜,目的就是通過數(shù)據(jù)流和控制流分析减俏,確定程序語義是合法的召烂、符合邏輯的,在元數(shù)據(jù)校驗的基礎上娃承,會對方法體進行校驗分析奏夫,保證方法在運行時不會對虛擬機有危害。

  • 符號引用驗證:這個步驟發(fā)生在虛擬機將符號引用轉化為直接引用過程中历筝,這個轉化動作發(fā)生在上圖類的生命周期所示的【連接】步驟的第三個階段--【解析】階段發(fā)生酗昼。可以看做是類對自身以外的信息進行匹配校驗梳猪。

準備

? ? ? ?這個步驟是正式為類變量分配內存并且設置初始值的階段麻削,這些變量使用的內存都將在方法區(qū)中進行分配。注意這里說的變量僅僅是static修飾的變量舔示,也就是跟類相關的變量碟婆,實例變量是在類進行實例化的時候,在Java堆中進行分配的惕稻。另外這里說的初始化并不是我們程序中定義的那些初始化的值竖共,它僅僅只是根據(jù)數(shù)據(jù)類型設置該類型的零值。例如i程序中定義了變量:

public static int value = 123;

? ? ? ?那么這里在初始化的時候俺祠,value的值此時是0公给,而value賦值123的操作是在程序被編譯之后進行的,123的賦值操作將會在后面【初始化】那一步進行的蜘渣。下面給出一些基本數(shù)據(jù)類型對應的零值:

數(shù)據(jù)類型 零值 數(shù)據(jù)類型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0

? ? ? ?上面的零值是通常情況下的賦值淌铐,但是還有一些特殊情況,例如用final修飾的變量蔫缸,它所修飾的變量在編譯階段都會生成ConstantValue屬性腿准,并且將該屬性的值指向程序所設置的值,如上面例子中的value拾碌,如果加上final吐葱,ConstantValue就會指向123,因此在準備階段校翔,虛擬機就會根據(jù)該屬性指向的值設置對應字段的值弟跑。

解析

? ? ? ?這個階段是虛擬機將常量池中的符號引用轉換為直接引用的過程,那么符號引用和直接引用到底是什么概念防症?

  • 符號引用(Symbolic Reference):它只是一種用來表示某種目標的一種標記孟辑,通過它能夠讓虛擬機定位到它所代指的目標即可哎甲,它可以是任何形式的字面量,它與虛擬機的內存布局無關饲嗽,只是一種概念上的存在炭玫,它所代指的目標不要求是否已經在內存中存在,不同虛擬機雖然內存布局不一樣貌虾,但是所接受的符號引用是一致的础嫡,所以說它是通用的。

  • 直接引用(Direct Reference):它可以是一個能夠直接指向目標的指針酝惧、相對偏移量或者一個能間接訪問到目標的句柄。它與具體的內存布局相關伯诬,它所指向的目標都已經是存在與內存中晚唇,是真實存在的。

? ? ? ?解析主要包括:類或接口的解析盗似、字段解析哩陕、類方法解析、接口方法解析赫舒。這里不再分析具體各個解析的概念悍及,想要了解詳情,可以查閱《深入理解Java虛擬機 第2版》的第七章第三小節(jié)解析部分接癌。

初始化

? ? ? ?到了這一步心赶,才開始執(zhí)行類中定義的Java程序代碼,在前面已經有過一步初始化步驟缺猛,在這一步缨叫,將會根據(jù)Java程序的主觀意愿去初始化變量和其他資源。初始化階段是執(zhí)行類構造器<cinit>()方法的過程荔燎。這里有一種情況需要說明:Java在定義靜態(tài)語句塊的時候耻姥,靜態(tài)語句塊中只能訪問到定義在該靜態(tài)語句塊之前的變量,對于定義在它之后的變量有咨,可以賦值琐簇,但是不能訪問,例如:

public class Test {
   static {
     i = 0;//可以編譯通過座享,正常賦值
     System.out.println(i);//編譯不通過婉商,提示“非法向前引用(Illegal forward reference)”
   }
   static int i = 1;
}

? ? ? ?而且<cinit>()方法執(zhí)行之前,父類的<cinit>()方法必須已經執(zhí)行完畢了征讲,所以說最先執(zhí)行的<cinit>()方法一定是Object類的据某,并且父類的靜態(tài)語句要先于子類的靜態(tài)語句執(zhí)行。

類加載器

? ? ? ?類加載器作用是實現(xiàn)類的加載動作诗箍,同時它也是區(qū)分類與類之間唯一性的重要依據(jù)癣籽。在Java中對于任意一個類挽唉,都需要由加載器和類的本身一同確定類在Java虛擬機中的唯一性。每一個類加載器都有一個獨立的命名空間筷狼。換句話說:如果要比較兩個類是不是“相等”瓶籽,首先得基于是同一個加載器加載出來的,否則兩個類肯定是“不相等”的埂材。即:同一份字節(jié)碼文件塑顺,被不同的加載器加載,那這兩個類仍然是“不相等”的俏险。

雙親委派模型

? ? ? ?在虛擬機中严拒,存在兩大類加載器:根加載器(BootStrap ClassLoader)和其他以Java實現(xiàn)的加載器。除根加載器之外竖独,其余的加載器都繼承自抽象類ClassLoader裤唠,并且由Java代碼實現(xiàn)。

  • 根加載器:是C++實現(xiàn)的莹痢,它主要是用于加載JAVA_Home\lib目錄中的類种蘸,或者被-Xbootclasspath參數(shù)指定的路徑。這里虛擬機識別的方式是按照名字識別的竞膳,換句話說:它識別的那些類都是已經定義好的航瞭,如果是不符合這些命名的,即使放進加載路徑中坦辟,也不會加載刊侯。

  • 擴展類加載器(Extension ClassLoader):主要是加載JAVA_HOME\lib\ext路徑下的類。

  • 應用程序加載類(Application ClassLoader):加載類路徑上指定的類庫长窄,如果程序中沒有自定的加載器滔吠,這個就是默認的加載器,用戶編寫的代碼挠日,一般都是通過它來加載疮绷。

? ? ? ?上面的三類加載器,是分層級的嚣潜,最底層是應用程序加載器冬骚,上面一層是擴展類加載器,最頂層的是根加載器懂算。雙親委派模型就是依據(jù)此結構建立的只冻,它具體工作過程是:如果一個加載器收到了類加載的請求,加載器并不會直接進行加載计技,而是把這個請求委派個父類加載器來完成喜德,只有父類加載器無法完成的時候,才到子類加載器中加載垮媒。另外需要明確一點:這里雖然說是“父子”關系舍悯,但是實際上并不是繼承關系航棱,而是通過組合方式實現(xiàn)的。在獲取類加載器的時候萌衬,可以通過getParent方法來獲取對應加載器的父類加載器饮醇。Application ClassLoader的父類加載器是Extension ClassLoader;Extension ClassLoader的父類加載器是BootStrap ClassLoader秕豫。但是如果我們通過Extension ClassLoader的getParent方法獲取父類加載器的時候朴艰,得到的會是null,這是因為根加載器是C++實現(xiàn)的混移,它是本地語言實現(xiàn)的祠墅。


類加載器雙親委派模型.png

雙親委派模型有一個好處,就是Java類隨著加載器的不同歌径,有了優(yōu)先級劃分饵隙,同時對于Java程序的安全性和穩(wěn)定性有了保障。試想:如果沒有雙親委派沮脖,隨便一個類都可以指定類加載器進行加載,那如果用戶自定義了一個Object類芯急,指定根加載器加載勺届,這就破壞了Java內部的繼承結構,Java內部娶耍,所有的類都是直接或間接繼承自Object免姿,這樣出現(xiàn)了多個Object類,Java體系內部榕酒,一些很基礎的行為就無法保證了(例如:hash胚膊,toString,equals等等這些行為)想鹰。這樣系統(tǒng)內部就會一片混亂紊婉。而有了雙親委派,就能夠保證越基礎的類辑舷,由越高級的類加載器去完成喻犁,例如:用戶嘗試編寫一個與rt.jar類庫中某個類重名的類,可以發(fā)現(xiàn)何缓,雖然可以編譯肢础,但是永遠不會被加載運行,因為在到根加載器驗證的時候就無法通過碌廓。

雙親委派模型在Java的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);

                   //下面這些步驟就是定義類加載器辽聊,并且記錄狀態(tài)的,暫時無視它
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看到股淡,代碼邏輯很清晰:

  • 先根據(jù)findLoadedClass確定是不是已經加載過

  • 如果沒有身隐,就委派父類加載器進行查找加載

  • 如果父類加載器沒有加載成功,就拋出ClassNotFoundException唯灵,并且調用本加載器的findClass方法進行加載

破壞雙親委派模型

? ? ? ?需要明白的是雙親委派模型不是一個強制性的約束贾铝,它只是一種推薦模式,Java中大都遵循這種模型埠帕,但是也會有例外垢揩,目前為止,雙親委派模型經歷過三次較大規(guī)模的“被破壞”情況敛瓷。其實與其說是“被破壞”叁巨,我更愿意稱它為“被改造”。因為帶來這些“破壞”的根本原因呐籽,主要還是因為雙親委派在有些特殊應用場景無法滿足的問題锋勺。

? ? ? ?第一次被破壞是JDK1.2之前,因為雙親委派模型是在JDK1.2之后才發(fā)布的狡蝶,ClassLoader在JDK1.0就已經存在了庶橱,因此在1.2版本發(fā)布后,需要兼容以前的代碼贪惹,1.2以后的ClassLoader添加一個protected方法findClass苏章。這么做的原因就是在于1.2以前,用戶繼承ClassLoader的唯一目的就是重寫loadClass方法奏瞬,那么虛擬機在調用類加載器的時候會調用加載器的私有方法loadClassInternal方法枫绅,該方法就一個邏輯:調用自己寫的loadClass方法,在1.2以后硼端,不提倡覆蓋loadClass方法并淋,建議重寫findClass方法,這樣在父類的loadClass加載失敗之后珍昨,會直接調用自身實現(xiàn)的findClass方法來加載预伺。以此保證新寫出來的類加載器是符合雙親委派模型的。

? ? ? ?第二次被破壞是模型本身缺陷造成的曼尊,因為該模型雖然可以讓越基礎的類由越高級的加載器完成加載酬诀,但是也限制了上層加載器中加載的類不能調用用戶的代碼,典型的例子就是JNDI服務骆撇,它在服務啟動的時候瞒御,需要調用各個廠商實現(xiàn)的不同接口,而該服務本身是放在rt.jar中的神郊,為了解決這個問題肴裙,引入了線程上下文類加載器趾唱,它可以通過Thread類的setContextClassLoader方法來設置,若線程創(chuàng)建時沒有指定蜻懦,就直接從父類中繼承一個甜癞,如果程序的全局環(huán)境都沒有設置,就采用默認的應用程序類加載器宛乃,有了個這種加載器悠咱,JNDI通過該線程去加載所需要的SPI代碼,即:父類加載器請求子類加載器去完成類的加載征炼,實際上就是雙親委派的逆向過程析既。常說的JNDI、JDBC等采用的都是這種方式谆奥。

? ? ? ?第三次“被破壞”是用戶對程序動態(tài)性追求而導致的眼坏。這里的動態(tài)性實際就是指:代碼熱替換、模塊熱部署等這些“熱點”概念酸些。就是希望在程序運行的時候在不需要重啟應用程序的情況下宰译,動態(tài)替換程序中的類。這里就不得不提OSGi魄懂,它是Sun公司提出的JSR-294囤屹、JSR-277規(guī)范與JCP組織的模塊化規(guī)范斗爭中的產物,最終Sun落敗給JSR-294規(guī)范(即:OSGi R4.2)逢渔,OSGi實現(xiàn)熱部署的關鍵就是:它有自定義的類加載器機制實現(xiàn),每個模塊在OSGi中都是一個Bundle乡括,它都有一個自己的類加載器肃廓,當需要更換模塊的時候,就將該Bundle連同所帶的類加載器一起換掉诲泌,從而達到熱部署的效果盲赊。而且在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結構敷扫,而是進一步發(fā)展為網狀結構哀蘑。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葵第,隨后出現(xiàn)的幾起案子绘迁,更是在濱河造成了極大的恐慌,老刑警劉巖卒密,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缀台,死亡現(xiàn)場離奇詭異,居然都是意外死亡哮奇,警方通過查閱死者的電腦和手機膛腐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門睛约,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哲身,你說我怎么就攤上這事辩涝。” “怎么了勘天?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵怔揩,是天一觀的道長。 經常有香客問我误辑,道長沧踏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任巾钉,我火速辦了婚禮翘狱,結果婚禮上,老公的妹妹穿的比我還像新娘砰苍。我一直安慰自己潦匈,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布赚导。 她就那樣靜靜地躺著茬缩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吼旧。 梳的紋絲不亂的頭發(fā)上凰锡,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音圈暗,去河邊找鬼掂为。 笑死,一個胖子當著我的面吹牛员串,可吹牛的內容都是我干的勇哗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寸齐,長吁一口氣:“原來是場噩夢啊……” “哼欲诺!你這毒婦竟也來了?” 一聲冷哼從身側響起渺鹦,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤扰法,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后毅厚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迹恐,經...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了殴边。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憎茂。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锤岸,靈堂內的尸體忽然破棺而出竖幔,到底是詐尸還是另有隱情,我是刑警寧澤是偷,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布拳氢,位于F島的核電站,受9級特大地震影響蛋铆,放射性物質發(fā)生泄漏馋评。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一刺啦、第九天 我趴在偏房一處隱蔽的房頂上張望留特。 院中可真熱鬧,春花似錦玛瘸、人聲如沸蜕青。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽右核。三九已至,卻和暖如春渺绒,著一層夾襖步出監(jiān)牢的瞬間贺喝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工宗兼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躏鱼,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓针炉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扳抽。 傳聞我的和親對象是個殘疾皇子篡帕,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內容