JVM - ClassLoader

1. 概述

類加載器實際定義了類的namespace。

package java.lang;

public abstract class ClassLoader {
     public Class loadClass(String name);

     protected Class defineClass(byte[] b);

     public URL getResource(String name);

     public Enumeration getResources(String name);

     public ClassLoader getParent();//
}

2.類加載方式之當前類加載器和指定類加載器

類的加載只有兩種加載方式胶惰,即當前類加載器加載(JVM自動行為本慕,無法干預)和指定類加載器加載(自己指定類加載器進行加載)踏堡。

2.1 當前類加載器

class A{
   B b;
}  

B的加載會使用A的類的類加載器進行加載碌燕,A的類加載器就是當前類加載器。這種類加載方式是JVM自動進行的倔幼,無法干預盖腿。

2.2 指定類加載器

代碼指定類加載器進行加載

3. 類加載器分類:定義類加載器和初始類加載器

  • 定義類加載器
    類的真實加載器,即通過class.getClassLoader()獲得的類加載器。
  • 初始類加載器
    類最先是由初始類加載器進行加載翩腐,初始類加載器并不一定是真正最后加載到類的加載器鸟款。

4. 各種類加載器

  1. BootStarpClassLoader
    Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的茂卦,它主要負責加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類何什。
  2. ExtClassLoader
    Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設置為Bootstrp loader.ExtClassLoader是用Java寫的,具體來說就是 sun.misc.Launcher$ExtClassLoader等龙,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext处渣,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類庫。
  3. AppClassLoader
    系統(tǒng)類加載器而咆,加載classpath下的類庫霍比。該類是Launcher類下的內(nèi)部類(包訪問權限),所以無法new 一個系統(tǒng)類加載器
    通過ClassLoader.getSystemClassLoader()能獲得該加載器暴备,該加載器是單例的。
    類加載器是有父類加載器的们豌,默認的父類加載器是系統(tǒng)類加載器
  4. 線程上下文類加載器
    Main線程的上下文類加載器是系統(tǒng)類加載器涯捻,線程的默認上下文類加載器是父線程的上下文類加載器。
  • 得到上下文類加載器
    public ClassLoader getContextClassLoader()
  • 設置上下文類加載器
    public void setContextClassLoader(ClassLoader cl)
    4.1 框架中一般怎么用上下文類記載器
    試想: 如果一個JNDI的提供方望迎,或者JAXP的提供方障癌,他們的SPI是通過bootstrap加載的,但是他們的實現(xiàn)類必須通過應用ClassLoader甚至是更下層的ClassLoader來加載辩尊。那么在其初始化的過程中涛浙,需要考慮如果獲取到部署了SPI實現(xiàn)的ClassLoader,而給出的方案是使用ContextClassLoader摄欲。比如轿亮。 在javax.xml.parsers.DocumentBuilderFactory中。

5. ClassLoader.loadClass

雙親制:

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

6. Class.forName和ClassLoader.loadClass過程

6.1 Class.forName

這里討論的Class.forNamepublic static Class<?> forName(String className) throws ClassNotFoundException
這里討論的ClassLoader.loadClasspublic Class<?> loadClass(String name) throws ClassNotFoundException

Class.forName是根據(jù)給定的類型全名從當前類加載器中加載指定的類型胸墙。
實現(xiàn)代碼:

    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

Reflection.getCallerClass()我注,獲取到調(diào)用Class.forName方法的類,隱含意義就是當前類加載器迟隅。加載的邏輯在native方法forName0中定義但骨,也就是forName進行的類加載行為已經(jīng)脫離了Java代碼的控制范圍,進入到了Java運行時環(huán)境把控的階段智袭。
以下是JDK實現(xiàn)的部分代碼:
Class.c中對應的實現(xiàn)邏輯:

JNIEXPORT jclass JNICALL
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
                              jboolean initialize, jobject loader)
{
    // 略
    cls = JVM_FindClassFromClassLoader(env, clname, initialize,
                                       loader, JNI_FALSE);
 done:
    if (clname != buf) {
        free(clname);
    }
    return cls;
}

實現(xiàn)細節(jié)在JVM_FindClassFromClassLoader中定義奔缠,可以看到調(diào)用Class.forName會使用JVM_FindClassFromClassLoader這個函數(shù)來進行類型加載,我們需要注意的是clname和loader這兩個變量吼野,一個是類的全限定名校哎,另一個是ClassLoader,而Class.forName所使用的ClassLoader是當前類加載器箫锤。
在jvm.cpp中FindClassFromClassLoader的對應實現(xiàn)是:

jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
  // Security Note:
  //   The Java level wrapper will perform the necessary security check allowing
  //   us to pass the NULL as the initiating class loader.
  klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
  // 略
}

SystemDictionary贬蛙,系統(tǒng)字典雨女,這個數(shù)據(jù)結構是保存Java加載類型的數(shù)據(jù)結構,如下圖所示阳准。


image.png

上圖黑色邊框中的內(nèi)容就是SystemDictionary氛堕,它是以類的全限定名再加上類加載器作為key,進而確定Class引用野蝇。
當在代碼中調(diào)用Class.forName(String name)或者由運行時Java進行類加載讼稚,比如:

          public void m() {
               B b = new B();
          }

對類型B的加載,就是運行時Java進行的類加載绕沈。
類型加載時锐想,以ClassLoader和需要加載的類型全限定名作為參數(shù)在SystemDictionary中進行查詢,如果能夠查詢到則返回乍狐。如果無法找到赠摇,則調(diào)用loader.loadClass(className)進行加載,這一步將進入到Java代碼中浅蚪。

對于loadClass而言藕帜,基本等同于loader.defineClass(loader.getResource(file).getBytes()),它做了兩件事惜傲,

  • 第一件洽故,通過資源定位到類文件,
  • 第二件盗誊,將類文件的字節(jié)流數(shù)組傳遞給defineClass進行構造Class實例时甚。而defineClass將再一次派發(fā)給運行時Java進行執(zhí)行荒适。
    字節(jié)流數(shù)組經(jīng)過ClassFileParser進行處理之后吻贿,生成了Class實例哑子,在返回Class實例前帐要,Java將name、loader和class的對應關系添加到SystemDictionary中赠橙,這樣在后續(xù)其他類型的加載過程中掉奄,就能夠快速找到這些類型,避免無謂的defineClass過程速兔。

一個類加載的過程,在運行時Java(JVM)和java代碼之間來回切換,有點復雜,我們畫一個簡單的圖來描述主要過程招拙,由于原有的類加載過程中還要處理并發(fā)問題,我們將這些內(nèi)容都去掉,只觀察類型加載的主要流程诉稍,如下圖所示。


image.png
  1. 調(diào)用Class.forName(className)方法,該方法會調(diào)用native的JVM實現(xiàn)心褐,調(diào)用前該方法會確定準備好需要加載的類名以及ClassLoader,將其傳遞給native方法
  2. 進入到JVM實現(xiàn)后桶至,首先會在SystemDictionary中根據(jù)類名和ClassLoader組成hash价涝,進行查詢,如果能夠命中覆山,則返回
  3. 如果加載到則返回
  4. 如果在SystemDictionary中無法命中吧享,將會調(diào)用Java代碼:ClassLoader.loadClass(類名),這一步將委派給Java代碼,讓傳遞的ClassLoader進行類型加載
  5. 以URLClassLoader為例,ClassLoader確定了類文件的字節(jié)流卵酪,但是該字節(jié)流如何按照規(guī)范生成Class對象幌蚊,這個過程在Java代碼中是沒有體現(xiàn)的,其實也就是要求調(diào)用ClassLoader.defineClass(byte[])進行解析類型,該方法將會再次調(diào)用native方法瘸羡,因為字節(jié)流對應Class對象的規(guī)范是定義在JVM實現(xiàn)中的
  6. 進入JVM實現(xiàn),調(diào)用SystemDictionary的resolve_stream方法粘昨,接受byte[]吞瞪,使用ClassFileParser進行解析
  7. SystemDictionary::define_instance_class
  8. 如果類型被加載了芍秆,將類名进统、ClassLoader和類型的實例引用添加到SystemDictionary中
  9. 返回
  10. 返回
  11. 從Java實現(xiàn)返回到Java代碼的defineClass眉菱,返回Class對象
  12. 返回給loadClass(Classname)方法
  13. 返回給Java實現(xiàn)的SystemDictionary迹栓,因為在resolve_class中調(diào)用的ClassLoader.loadClass。這里會做出一個判斷俭缓,如果加載Class的ClassLoader并非傳遞給resolve_class的ClassLoader克伊,那么會將類名、傳遞給resolve_class的ClassLoader以及類型的實例引用添加到SystemDictionary中
  14. 返回給Class.forName類型實例

上述的過程比較復雜华坦,但是簡化理解一下它所做的工作愿吹,我們將SystemDictionary記作緩存,Class.forName或者說Java默認的類型加載過程是:*

  1. 首先根據(jù)ClassLoader惜姐,我們稱之為initialClassLoader和類名查找緩存犁跪,如果緩存有椿息,則返回;
  2. 如果緩存沒有坷衍,則調(diào)用ClassLoader.loadClass(類名)寝优,加載到類型后,保存<類名枫耳,真實加載類的ClassLoader乏矾,類型引用>到緩存,這里真實加載類的ClassLoader我們可以叫做defineClassLoader迁杨;
  3. 返回的類型在交給Java之前钻心,將會判斷defineClassLoader是否等于initialClassLoader,如果不等铅协,則新增<類名捷沸,initialClassLoader,類型引用>到緩存警医。
  4. 這里區(qū)分initialClassLoader和defineClassLoader的原因在于亿胸,調(diào)用initialClassLoader的loadClass,可能最終委派給其他的ClassLoader進行了加載预皇。

6.2 ClassLoader.loadClass(String className)

我們在分析了Class.forName之后侈玄,再看ClassLoader.loadClass()就會變得簡單很多,這個ClassLoader就是一個指定類加載器吟温,而ClassLoader.loadClass()只是相當于一個簡單的方法調(diào)用序仙。
根據(jù)上圖所示,該過程開始于第4步鲁豪,沒有前3步潘悼,該過程簡單說就是:調(diào)用ClassLoader.loadClass(類名),加載到類型后爬橡,保存<類名治唤,真實加載類的ClassLoader,類型引用>到緩存糙申,這里真實加載類的ClassLoader我們可以叫做defineClassLoader宾添。也就是,調(diào)用ClassLoader.loadClass(類名)之后柜裸,并不一定會在緩存中生成一條<類名缕陕,ClassLoader,類型引用>的記錄疙挺,但是一定會生成一條<類名扛邑,真實加載類的ClassLoader,類型引用>的記錄铐然。(自己附注:實際上最少在JDK8之后loadClass也先到系統(tǒng)字典中查詢是否已創(chuàng)建)

6.3 ClassLoader.findLoadedClass(String className)

該方法是protected final修飾的方法蔬崩,也就是ClassLoader的子類可以內(nèi)部使用恶座,但是無法通過ClassLoader.findLoadedClass直接調(diào)用。
這個方法一直感覺很奇怪舱殿,從名稱上看就是查詢這個ClassLoader加載過的Class奥裸,如果加載過了,那么就返回類型實例沪袭。

7. 怎么創(chuàng)建一個類加載器

URLClassLoader基本能滿足一些個性的類加載需求湾宙,如果還不滿足,可以實現(xiàn)自己的類加載器冈绊。

  1. extend ClassLoader
  2. 覆蓋protected Class<?> findClass(String name) throws ClassNotFoundException
    在findClass中實現(xiàn)查找類字節(jié)碼的邏輯侠鳄,并調(diào)用protected final Class<?> defineClass(String name, byte[] b, int off, int len)得到類。
  3. 注意設置父類加載器死宣。
    默認的父類加載器是系統(tǒng)類加載器伟恶,如果設置父類加載器為null,真實的父類加載器是啟動類加載器毅该。

上面步驟的類加載器符合雙親制加載規(guī)范博秫。Override ClassLoader.loadClass實現(xiàn)了雙親制和緩存細節(jié),不建議打破眶掌。

5. Class.getResource(String path)

path不以’/'開頭時挡育,默認是從此類所在的包下取資源;
path 以’/'開頭時朴爬,則是從ClassPath根下獲燃春;
TestMain.class.getResource("/") == t.getClass().getClassLoader().getResource("")

7. 各種錯誤

遇到類加載器問題時召噩,可以嘗試使用下面的表格進行問題排查母赵。

類找不到 加載了不正確的類 多于一個類被加載
ClassNotFoundException NoClassDefFoundError IncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessError ClassCastException LinkageError
IDE class lookup (Ctrl+Shift+T in Eclipse) 或者 find . -name "*.jar" -exec jar -tf {} ; \ grep DateUtils 使用middelware-detector 通過在啟動參數(shù)中加 -verbose:class,觀察加載的類來自哪個jar包 使用middelware-detector

7.1 ClassNotFoundException 和NoClassDefFoundError的區(qū)別


sometimes error on static initializer block can also result in NoClassDefFoundError.

7.2 LinkageError

LinkageError 需要觀察哪個類被不同的類加載器加載了具滴,在哪個方法或者調(diào)用處發(fā)生(交匯)的凹嘲,然后才能想解決方法,解決方法無外乎兩種构韵。

  1. 還是不同的類加載器加載施绎,但是相互不再交匯影響,這里需要針對發(fā)生問題的地方做一些改動贞绳,比如更換實現(xiàn)方式,避免出現(xiàn)上述問題致稀;
  2. 沖突的類需要由一個Parent類加載器進行加載冈闭。LinkageError 和ClassCastException 本質(zhì)是一樣的,加載自不同類加載器的類型抖单,在同一個類的方法或者調(diào)用中出現(xiàn)萎攒,如果有轉(zhuǎn)型操作那么就會拋 ClassCastException 遇八,如果是直接的方法調(diào)用處的參數(shù)或者返回值解析,那么就會產(chǎn)生 LinkageError 耍休。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刃永,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子羊精,更是在濱河造成了極大的恐慌斯够,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喧锦,死亡現(xiàn)場離奇詭異读规,居然都是意外死亡,警方通過查閱死者的電腦和手機燃少,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門束亏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阵具,你說我怎么就攤上這事碍遍。” “怎么了阳液?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵怕敬,是天一觀的道長。 經(jīng)常有香客問我趁舀,道長赖捌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任矮烹,我火速辦了婚禮越庇,結果婚禮上,老公的妹妹穿的比我還像新娘奉狈。我一直安慰自己卤唉,他們只是感情好,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布仁期。 她就那樣靜靜地躺著桑驱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跛蛋。 梳的紋絲不亂的頭發(fā)上熬的,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音赊级,去河邊找鬼押框。 笑死,一個胖子當著我的面吹牛理逊,可吹牛的內(nèi)容都是我干的橡伞。 我是一名探鬼主播盒揉,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兑徘!你這毒婦竟也來了刚盈?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤挂脑,失蹤者是張志新(化名)和其女友劉穎藕漱,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體最域,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡谴分,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了镀脂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牺蹄。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖薄翅,靈堂內(nèi)的尸體忽然破棺而出沙兰,到底是詐尸還是另有隱情,我是刑警寧澤翘魄,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布鼎天,位于F島的核電站,受9級特大地震影響暑竟,放射性物質(zhì)發(fā)生泄漏斋射。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一但荤、第九天 我趴在偏房一處隱蔽的房頂上張望罗岖。 院中可真熱鬧,春花似錦腹躁、人聲如沸桑包。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哑了。三九已至,卻和暖如春烧颖,著一層夾襖步出監(jiān)牢的瞬間弱左,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工炕淮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留科贬,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像榜掌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乘综,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容