細(xì)說JVM(類加載器)

一往毡、類加載器的基本概念

顧名思義靶溜,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中墨技。一般來說扣汪,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)崭别。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個實例茅主。每個這樣的實例用來表示一個 Java 類诀姚。通過此實例的 newInstance()方法就可以創(chuàng)建出該類的一個對象。實際的情況可能更加復(fù)雜呀打,比如 Java 字節(jié)代碼可能是通過工具動態(tài)生成的贬丛,也可能是通過網(wǎng)絡(luò)下載的豺憔。

二够庙、類與類加載器

對于任意一個類,都需要加載它的類加載器和這個類本身來確定這個類在Java虛擬機中的唯一性暮屡,每一個類加載器都有一個獨立的類名稱空間褒纲。也就是說莺掠,如果比較兩個類是否是同一個類读宙,除了這比較這兩個類本身的全限定名是否相同之外,還要比較這兩個類是否是同一個類加載器加載的唇兑。即使同一個類文件兩次加載到同一個虛擬機中扎附,但如果是由兩個不同的類加載器加載的留夜,那這兩個類仍然不是同一個類。
這個相等性比較會影響一些方法碍粥,比如Class對象的equals()方法嚼摩、isAssignableFrom()方法、isInstance()方法等蜂厅,還有instanceof關(guān)鍵字做對象所屬關(guān)系判定等掘猿。下面的代碼演示了不同的類加載器對instanceof關(guān)鍵字的影響:

package temp;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception{
        ClassLoader loader=new ClassLoader() {
            @Override
            public Class<?> loadClass(String name)throws ClassNotFoundException{
                try{
                    String filename=name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is=getClass().getResourceAsStream(filename);
                    if(is==null){
                        return super.loadClass(name);
                    }
                    byte[] b=new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch(IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj=loader.loadClass("temp.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());
        System.out.println(obj instanceof temp.ClassLoaderTest);
    }
}

運行結(jié)果:


TIM截圖20180809165411.png

這里構(gòu)造了一個簡單的類加載器稠通,它可以加載與自己在同一個路徑下的Class文件改橘。然后使用這個類加載器去加載全限定名是temp.ClassLoaderTest的類飞主,并實例化了這個類的對象碌识。從第一行輸出可以看出虱而,這個對象確實是temp.ClassLoaderTest類的一個實例,我們打印了一下對象obj的類加載器和ClassLoaderTest的類加載器魁瞪,發(fā)現(xiàn)確實是不同的兩個不同的類加載器导俘,最后輸出表明在做instanceof檢查時出現(xiàn)了false剔蹋,這是因為這時虛擬機中有兩個temp.ClassLoaderTest類滩租,一個是系統(tǒng)應(yīng)用程序類加載器加載的,另一個是自定義的類加載器加載的猎莲,這兩個類雖然來自同一個Class文件著洼,但是加載它們的類加載器不同而叼,導(dǎo)致類型檢查時結(jié)果是false

三、雙親委派模型

Java 中的類加載器大致可以分成兩類匕垫,一類是系統(tǒng)提供的绊困,另外一類則是由 Java 應(yīng)用開發(fā)人員編寫的。系統(tǒng)提供的類加載器主要有下面三個:

  • 啟動類加載器:負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的煤蹭,或者被-Xbootclasspath參數(shù)所指定的路徑中的硝皂,并且是虛擬機識別的類庫加載到虛擬機內(nèi)存中贫途。啟動類加載器無法被Java程序直接引用丢早,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器傀缩,那直接使用null代替即可赡艰。
  • 擴展類加載器:這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn)斤葱,負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄下的揖闸,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫汤纸,開發(fā)者可以直接使用擴展類加載器贮泞。
  • 應(yīng)用程序類加載器:這個類加載器是由sun.misc.Launcher$AppClassLoader實現(xiàn)的啃擦。由于這個類加載器是ClassLoader中的getSystemClassLoader方法的返回值饿悬,所以也叫系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑上所指定的類庫言询,開發(fā)者可以直接使用這個類加載器运杭,如果應(yīng)用程序中沒有自定義過自己的類加載器函卒,一般情況下這個就是程序中默認(rèn)的類加載器报嵌。

用戶的應(yīng)用程序就是在這三個類加載器的配合下加載的。不過腕巡,用戶還可以加入自己的類加載器绘沉,這些類加載器的關(guān)系如下圖:


image001.jpg

這種類加載的層次關(guān)系车伞,稱為類加載器的雙親委派模型喻喳。雙親委派模型要求除了頂層的啟動類加載器之外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器谦去。不過這個父子關(guān)系不是通過繼承實現(xiàn)的鳄哭,而是使用組合關(guān)系來復(fù)用父加載器的代碼。

雙親委派模型的工作過程如下:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類飘痛,而是把這個請求委派給父類加載器去完成容握,每一個層次的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啟動類加載器中塑猖,只有當(dāng)父類加載器反饋自己無法完成這個加載請求(它的搜索范圍內(nèi)沒找到這個類)時羊苟,自加載器才會嘗試自己加載感憾。

雙親委派模型是為了保證 Java 核心庫的類型安全。所有 Java 應(yīng)用都至少需要引用 java.lang.Object類凉倚,也就是說在運行的時候稽寒,java.lang.Object這個類需要被加載到 Java 虛擬機中趟章。如果這個加載過程由 Java 應(yīng)用自己的類加載器來完成的話尤揣,很可能就存在多個版本的 java.lang.Object類,而且這些類之間是不兼容的负芋。通過雙親委派模型,對于 Java 核心庫的類的加載工作由啟動類加載器來統(tǒng)一完成莽龟,保證了 Java 應(yīng)用所使用的都是同一個版本的 Java 核心庫的類毯盈,是互相兼容的病袄。

不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機中脑奠,只需要用不同的類加載器來加載它們即可宋欺。不同類加載器加載的類之間是不兼容的胰伍,這就相當(dāng)于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間。

四祷杈、深入探討雙親委派模型

雙親委派模型雖然保障了Java核心類庫的安全問題吠式,但是雙親委派模型也有其缺點,那就是如果基礎(chǔ)類又要調(diào)用用戶的代碼特占,那該怎么辦云茸?
比如我們經(jīng)常使用的JDBC技術(shù),學(xué)習(xí)過JDBC的應(yīng)該知道JDBC只是一組接口規(guī)范,具體的實現(xiàn)是由數(shù)據(jù)庫廠商實現(xiàn)的亡容,那么JDBC的接口代碼存在于核心類庫中,是由啟動類加載器加載的,但是JDBC的實現(xiàn)代碼是由各個廠商提供脚囊,是由系統(tǒng)類加載器加載悔耘。啟動類加載器是無法無法找到 JDBC接口 的實現(xiàn)類的,因為它只加載 Java 的核心庫缓艳。它也不能代理給系統(tǒng)類加載器郎任,因為它是系統(tǒng)類加載器的祖先類加載器备籽。也就是說车猬,類加載器的雙親委派模型無法解決這個問題珠闰。

看到這里你可能會產(chǎn)生一個疑問瘫辩,那就是為啥上層的類加載器加載的類無法訪問下層類加載器加載的類,但是下層的類加載器加載的類可以訪問上層類加載器加載的類承绸,比如:我們寫的類Person由系統(tǒng)類加載器加載军熏,String類由啟動類加載器加載卷扮,也就是說Person類中可以訪問到String,但是String類中無法訪問到Person(這里是一個不太恰當(dāng)?shù)睦幽︶#驗槲覀儫o法具體的測試String類中是否可以訪問到Person類)或衡。

我在看書的時候就產(chǎn)生了上面的疑問,經(jīng)過谷歌老師的指導(dǎo)偷办,我終于找到了問題的答案:

首先是兩個術(shù)語:在前面介紹類加載器的雙親委派模型的時候椒涯,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類回梧。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器狱意,有可能不是同一個。真正完成類的加載工作是通過調(diào)用defineClass來實現(xiàn)的财骨;而啟動類的加載過程是通過調(diào)用loadClass來實現(xiàn)的隆箩。前者稱為一個類的定義加載器(defining loader)羔杨,后者稱為初始加載器(initiating loader)。

注意:初始類加載器對于一個類來說經(jīng)常不是一個理澎,比如String類在加載的過程中糠爬,先是交給系統(tǒng)類加載器加載举庶,但是系統(tǒng)類加載器代理給了擴展類加載期,擴展類加載器又代理給了引導(dǎo)類加載器殴玛,最后由引導(dǎo)類加載器加載完成添祸,那么這個過程中的定義類加載器就是引導(dǎo)類加載器刃泌,但是初始類加載器是三個(系統(tǒng)類加載器署尤、擴展類加載器曹体、引導(dǎo)類加載器)硝烂,因為這三個類加載器都調(diào)用了loadClass方法,而最后的引導(dǎo)類加載器還調(diào)用了defineClass方法滞谢。

JVM為每個類加載器維護(hù)的一個“表”,這個表記錄了所有以此類加載器為“初始類加載器”(而不是定義類加載器狮杨,所以一個類可以存在于很多的命名空間中)加載的類的列表。屬于同一個列表的類可以互相訪問清寇。這就可以解釋為什么上層的類加載器加載的類無法訪問下層類加載器加載的類,但是下層的類加載器加載的類可以訪問上層類加載器加載的類?的疑問了搅方。

在 Java 虛擬機判斷兩個類是否相同的時候姨涡,使用的是類的定義加載器。也就是說匈仗,哪個類加載器啟動類的加載過程并不重要,重要的是最終定義這個類的加載器火架。兩種類加載器的關(guān)聯(lián)之處在于:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類com.example.Inner骡男,則由類 com.example.Outer的定義加載器負(fù)責(zé)啟動類 com.example.Inner的加載過程。

五骚亿、解決雙親委派模型的缺陷——線程上下文類加載器

線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法getContextClassLoader()setContextClassLoader(ClassLoader cl)用來獲取和設(shè)置線程的上下文類加載器。如果沒有通過setContextClassLoader(ClassLoader cl)方法進(jìn)行設(shè)置的話泥技,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運行的初始線程的上下文類加載器是系統(tǒng)類加載器店茶。在線程中運行的代碼可以通過此類加載器來加載類和資源。

通過使用線程上下文類加載器可以實現(xiàn)父類加載器請求子類加載器去完成類加載的動作。

這里并沒有講解為啥線程上下文類加載器可以打破雙親委派模型鸯檬?為啥可以逆向使用類加載器?庐冯,如果想要了解,這里有篇文章可以參考一下:真正理解線程上下文類加載器(多案例分析)

六栖茉、另外一種加載類的方法:Class.forName

Class.forName是一個靜態(tài)方法尘应,同樣可以用來加載類苍鲜。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)Class.forName(String className)。第一種形式的參數(shù) name表示的是類的全名坯屿;initialize表示是否初始化類电湘;loader表示加載時使用的類加載器。第二種形式則相當(dāng)于設(shè)置了參數(shù)initialize的值為trueloader的值為當(dāng)前類的類加載器劫拢。Class.forName的一個很常見的用法是在加載數(shù)據(jù)庫驅(qū)動的時候妹沙。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來加載 Apache Derby 數(shù)據(jù)庫的驅(qū)動牵寺。

七趣斤、開發(fā)自己的類加載器

一般來說唬渗,自己開發(fā)的類加載器只需要覆寫findClass(String name)方法即可。java.lang.ClassLoader類的方法loadClass()封裝了雙親委派模型的實現(xiàn)。該方法會首先調(diào)用findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話浴滴,會調(diào)用父類加載器的 loadClass()方法來嘗試加載該類;如果父類加載器無法加載該類的話屡限,就調(diào)用findClass()方法來查找該類。因此眶诈,為了保證類加載器都正確實現(xiàn)代理模式东帅,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法,而是覆寫findClass()方法檩淋。示例如下:

public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 編寫findClass方法的邏輯
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 獲取類的class文件字節(jié)數(shù)組
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class對象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 編寫獲取class文件并轉(zhuǎn)換為字節(jié)碼流的邏輯
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {
        // 讀取類文件的字節(jié)
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 讀取類文件的字節(jié)碼
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 類文件的完全路徑
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //創(chuàng)建自定義文件類加載器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //加載指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //輸出結(jié)果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唯袄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖酪我,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逞带,死亡現(xiàn)場離奇詭異,居然都是意外死亡未妹,警方通過查閱死者的電腦和手機歪赢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事媳拴。” “怎么了帆赢?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仪搔。 經(jīng)常有香客問我,道長偏陪,這世上最難降的妖魔是什么笛谦? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮恳邀,結(jié)果婚禮上轩娶,老公的妹妹穿的比我還像新娘鳄抒。我一直安慰自己椰弊,他們只是感情好秉版,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布清焕。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咏删,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音些己,去河邊找鬼逼庞。 笑死掌逛,一個胖子當(dāng)著我的面吹牛皿伺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宰翅,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤倍阐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后咐蚯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體期奔,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡济欢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年梆惯,在試婚紗的時候發(fā)現(xiàn)自己被綠了怯屉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锨络。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡羡儿,死狀恐怖弹囚,靈堂內(nèi)的尸體忽然破棺而出毁渗,到底是詐尸還是另有隱情,我是刑警寧澤恍风,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布糊饱,位于F島的核電站柜去,受9級特大地震影響根盒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜册赛,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一票堵、第九天 我趴在偏房一處隱蔽的房頂上張望痰驱。 院中可真熱鬧,春花似錦叫潦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窥摄,卻和暖如春哨苛,著一層夾襖步出監(jiān)牢的瞬間移国,已是汗流浹背迹缀。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工砚蓬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摩梧。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓笙纤,卻偏偏與公主長得像省容,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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