深入理解Java ClassLoader及在 JavaAgent 中的應(yīng)用

背景

眾所周知, Java 或者其他運(yùn)行在 JVM(java 虛擬機(jī))上面的程序都需要最終便以為字節(jié)碼,然后被 JVM加載運(yùn)行,那么這個(gè)加載到虛擬機(jī)的過(guò)程就是 classloader 類(lèi)加載器所干的事情.直白一點(diǎn),就是 通過(guò)一個(gè)類(lèi)的全限定類(lèi)名稱(chēng)來(lái)獲取描述此類(lèi)的二進(jìn)制字節(jié)流 的過(guò)程.

雙親委派模型

說(shuō)到 Java 的類(lèi)加載器,必不可少的就是它的雙親委派模型,從 Java 虛擬機(jī)的角度來(lái)看,只存在兩種不同的類(lèi)加載器:

  1. 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader), 由 C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分.
  2. 其他的類(lèi)加載器,都是由 Java 實(shí)現(xiàn),在虛擬機(jī)的外部,并且全部繼承自java.lang.ClassLoader

在 Java 內(nèi)部,絕大部分的程序都會(huì)使用 Java 內(nèi)部提供的默認(rèn)加載器.

啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader)

負(fù)責(zé)將$JAVA_HOME/lib或者 -Xbootclasspath 參數(shù)指定路徑下面的文件(按照文件名識(shí)別,如 rt.jar) 加載到虛擬機(jī)內(nèi)存中.啟動(dòng)類(lèi)加載器無(wú)法直接被 java 代碼引用,如果需要把加載請(qǐng)求委派給啟動(dòng)類(lèi)加載器,直接返回null即可.

擴(kuò)展類(lèi)加載器(Extension ClassLoader)

負(fù)責(zé)加載$JAVA_HOME/lib/ext 目錄中的文件,或者java.ext.dirs 系統(tǒng)變量所指定的路徑的類(lèi)庫(kù).

應(yīng)用程序類(lèi)加載器(Application ClassLoader)

一般是系統(tǒng)的默認(rèn)加載器,比如用 main 方法啟動(dòng)就是用此類(lèi)加載器,也就是說(shuō)如果沒(méi)有自定義過(guò)類(lèi)加載器,同時(shí)它也是getSystemClassLoader() 的返回值.

這幾種類(lèi)加載器的工作流程被抽象成一個(gè)模型,就是雙親委派模型.

image.png

工作流程:

  1. 收到類(lèi)加載的請(qǐng)求
  2. 首先不會(huì)自己嘗試加載此類(lèi),而是委托給父類(lèi)的加載器去完成.
  3. 如果父類(lèi)加載器沒(méi)有,繼續(xù)尋找父類(lèi)加載器.
  4. 搜索了一圈,發(fā)現(xiàn)都找不到,然后才是自己嘗試加載此類(lèi).

這基本就是雙親委派模型.

但是這種模型只是一種推薦的方式,并不是強(qiáng)制的,你也可以嘗試打破這種規(guī)則.
自所以這樣約定,還是有一定的好處的, Java 類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系.
比如自己定義了java.lang.Object 對(duì)象,那么按照上面的流程,他永遠(yuǎn)都是被啟動(dòng)類(lèi)加載器加載的rt.jar 中的那個(gè)類(lèi),而不是自己定義的這個(gè)類(lèi),這樣就保證了兄運(yùn)行的穩(wěn)定,否則,可能變得非常混亂,可以隨意改寫(xiě)任何類(lèi).

在 JavaAgent 中的應(yīng)用

大多數(shù)情況下,其實(shí)我們并不需要知道這些,因?yàn)槟愕某绦蛞矔?huì)運(yùn)行的非常正常,雖然像Tomcat,Spring Boot 都有自己定義的類(lèi)加載器,但是我們?cè)诓挥藐P(guān)心的情況下也會(huì)運(yùn)行的好好地.

那么類(lèi)加載器可以被運(yùn)行在哪些地方呢?

  • 從遠(yuǎn)程(或者文件)加載類(lèi),有時(shí)候需要加載的類(lèi)可能并不是在當(dāng)前的 classpath, 可能需要自己定義類(lèi)加載器去加載.
  • 自己想實(shí)現(xiàn)一個(gè)JavaAgent來(lái)增強(qiáng)字節(jié)碼的時(shí)候.

JavaAgent 的使用后續(xù)文章補(bǔ)上.先上一張圖.

image.png
  • 頂層是應(yīng)用代碼實(shí)際運(yùn)行的 ClassLoader, 可能是Application ClassLoader, 也有可能是 tomcat 的webapp ClassLoader 或者其他容器自定義的類(lèi)加載器,總是是真實(shí) 的用戶(hù)編寫(xiě)的代碼運(yùn)行的 classloader.

  • 我們?nèi)绻?code>javaagent中增強(qiáng)用戶(hù)或者用戶(hù)使用的包進(jìn)行增強(qiáng)的話(huà),必須實(shí)現(xiàn)一個(gè)自定義的 classloader 來(lái)"繼承"(委派)應(yīng)用代碼的類(lèi)加載器.為什么?

  • javaagent 的代碼永遠(yuǎn)都是被應(yīng)用類(lèi)加載器( Application ClassLoader)所加載,和應(yīng)用代碼的真實(shí)加載器無(wú)關(guān),舉個(gè)栗子,當(dāng)前運(yùn)行在 tomcat 中的代碼是webapp ClassLoader 加載的,如果啟動(dòng)參數(shù)加上-javaagent, 這個(gè) javaagent 還是在Application ClassLoader中加載的.

  • 按照上面的雙親委派模型,如果我們?cè)?javaagent 中想要訪(fǎng)問(wèn)應(yīng)用里面的 api 包或者類(lèi),這是不可能的,因?yàn)榘凑针p親委派模型,通俗來(lái)說(shuō)就是,子加載器可以訪(fǎng)問(wèn)父加載器中的類(lèi),但是反過(guò)來(lái)就行不通.

那么這個(gè)時(shí)候有沒(méi)有辦法能夠做到呢?

  • 我們可以自定義自己的類(lèi)加載器繼承應(yīng)用代碼類(lèi)加載器(可以在 javaagent 中完成, javaagent 每加載一個(gè)類(lèi),就會(huì)回調(diào)傳回真實(shí)的類(lèi)加載器),然后我們?cè)?code>Application ClassLoader 中用自定義的類(lèi)加載器去加載子類(lèi),并創(chuàng)建好實(shí)例(newInstance()), 將實(shí)例的引用保存 在變量中.

  • 真實(shí)運(yùn)行的時(shí)候,就會(huì)通過(guò)這個(gè)變量,去訪(fǎng)問(wèn)我們自定義加載器的內(nèi)容,又由于我們的自定義類(lèi)加載器是繼承自應(yīng)用代碼的類(lèi)加載器的,所以自定義類(lèi)加載器中的代碼可以訪(fǎng)問(wèn)應(yīng)用的代碼.

總結(jié)一句就是,父類(lèi)加載器無(wú)法加載子類(lèi)加載器的類(lèi),但是可以持有子類(lèi)加載器所加載類(lèi)的實(shí)例,從而實(shí)現(xiàn)父類(lèi)加載器的代碼可以調(diào)用子類(lèi)加載器的代碼的形式

貌似比較抽象,后面會(huì)補(bǔ)上詳細(xì)的例子供參考.

例子

針對(duì)上面的情形,我們定義一個(gè)例子,可以詳細(xì)解釋 ClassLoader 的加載使用,

  1. 假如我們有如下的 ClassLoader,FooClassLoader:
package com.example.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author lican
 */
public class FooClassLoader extends ClassLoader {

    private static final String NAME = "/Users/lican/git/test/foo/";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            String s = name.substring(name.lastIndexOf(".") + 1) + ".class";
            File file = new File(NAME + s);
            try (FileInputStream fileInputStream = new FileInputStream(file)) {
                byte[] b = new byte[fileInputStream.available()];
                fileInputStream.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return loadedClass;
    }

}
  1. 被加載的類(lèi)定義,然后我們將這個(gè)類(lèi)放到不是源代碼的路徑比如我放到
    /Users/lican/git/test/foo/這里的,主要是方便測(cè)試.

package com.example.test;

public class FooTest {

    public String getFoo() {
        return "foo";
    }
}

然后測(cè)試程序?yàn)?

package com.example.test;

import java.lang.reflect.Method;

/**
 * @author lican
 */
public class ClassLoaderTest {

    private Object fooTestInstance;
    private FooClassLoader fooClassLoader = new FooClassLoader();


    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoaderTest = new ClassLoaderTest();
        classLoaderTest.initAndLoad();
        Object fooTestInstance = classLoaderTest.getFooTestInstance();
        System.out.println(fooTestInstance.getClass().getClassLoader());


        Method getFoo = fooTestInstance.getClass().getMethod("getFoo");
        System.out.println(getFoo.invoke(fooTestInstance));

        System.out.println(classLoaderTest.getClass().getClassLoader());
    }

    private void initAndLoad() throws Exception {
        Class<?> aClass = Class.forName("com.example.test.FooTest", true, fooClassLoader);
        fooTestInstance = aClass.newInstance();
    }

    public Object getFooTestInstance() {
        return fooTestInstance;
    }
}

我們用FooClassLoader來(lái)加載com.example.test.FooTest, 然后在 AppClassLoader中持有引用.被后續(xù)使用.

引用

  • 深入理解 Java 虛擬機(jī)(第二版)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谈跛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吟税,老刑警劉巖犬辰,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磕诊,死亡現(xiàn)場(chǎng)離奇詭異填物,居然都是意外死亡纹腌,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)滞磺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)升薯,“玉大人,你說(shuō)我怎么就攤上這事击困∠雅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵阅茶,是天一觀(guān)的道長(zhǎng)蛛枚。 經(jīng)常有香客問(wèn)我,道長(zhǎng)脸哀,這世上最難降的妖魔是什么蹦浦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮企蹭,結(jié)果婚禮上白筹,老公的妹妹穿的比我還像新娘智末。我一直安慰自己谅摄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布系馆。 她就那樣靜靜地躺著送漠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪由蘑。 梳的紋絲不亂的頭發(fā)上闽寡,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音尼酿,去河邊找鬼爷狈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裳擎,可吹牛的內(nèi)容都是我干的涎永。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鹿响,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼羡微!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起惶我,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妈倔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绸贡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體盯蝴,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毅哗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了结洼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黎做。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖松忍,靈堂內(nèi)的尸體忽然破棺而出蒸殿,到底是詐尸還是另有隱情,我是刑警寧澤鸣峭,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布宏所,位于F島的核電站,受9級(jí)特大地震影響摊溶,放射性物質(zhì)發(fā)生泄漏爬骤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一莫换、第九天 我趴在偏房一處隱蔽的房頂上張望霞玄。 院中可真熱鬧,春花似錦拉岁、人聲如沸坷剧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惫企。三九已至,卻和暖如春陵叽,著一層夾襖步出監(jiān)牢的瞬間狞尔,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工巩掺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偏序,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓胖替,卻偏偏與公主長(zhǎng)得像研儒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刊殉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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