從jdk源碼深入剖析java類加載機(jī)制

類加載過程整體分析

當(dāng)我們用java命令運(yùn)行某個類的main函數(shù)啟動程序時,首先需要通過類加載器把主類加載到 JVM

public class Math {
    public static final int initData = 666;
    public static User user = new User();

    public int compute() { //一個方法對應(yīng)一塊棧幀內(nèi)存區(qū)域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

通過Java命令執(zhí)行代碼的大體流程如下:


類加載整體流程.png

從上圖我們可以看出發(fā)起調(diào)用的地方是操作系統(tǒng)底層幫我們實(shí)現(xiàn)的,引導(dǎo)類加載器也不是由java編寫的察藐。

在真正加載我們要運(yùn)行的類之前要做很多準(zhǔn)備工作,這其中很多地方都不是java語言所能處理的舟扎,因此不必做過多的探究分飞。

那么類加載在加載類的過程中發(fā)生了哪些事情呢?大概可以分為以下七個階段:

類加載幾大階段.png

  • 加載:在硬盤上查找并通過IO讀入字節(jié)碼文件睹限,使用到類時才會加載譬猫,例如調(diào)用類的main()方法,new對象等等羡疗,在加載階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象染服,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
  • 驗(yàn)證:校驗(yàn)字節(jié)碼文件的正確性
  • 準(zhǔn)備:給類的靜態(tài)變量分配內(nèi)存,并賦予默認(rèn)值
  • 解析:將符號引用替換為直接引用叨恨,該階段會把一些靜態(tài)方法(符號引用柳刮,比如main()方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這是所謂的靜態(tài)鏈接過程(類加載期間完成)痒钝,動態(tài)鏈接是在程序運(yùn)行期間完成的將符號引用替換為直接引用
  • 初始化:對類的靜態(tài)變量初始化為指定的值秉颗,執(zhí)行靜態(tài)代碼塊
    類加載過程.png

    PS:類被加載到方法區(qū)中后主要包含 運(yùn)行時常量池、類型信息送矩、字段信息蚕甥、方法信息、類加載器的引用栋荸、對應(yīng)class實(shí)例的引用等信息菇怀。
    類加載器的引用:這個類到類加載器實(shí)例的引用
    對應(yīng)class實(shí)例的引用:類加載器在加載類信息放到方法區(qū)中后夷家,會創(chuàng)建一個對應(yīng)的Class 類型的對象實(shí)例放到堆(Heap)中, 作為開發(fā)人員訪問方法區(qū)中類定義的入口和切入點(diǎn)。
    那么類是在jvm啟動時就全部加載了嗎敏释?

答案是否定的,事實(shí)上摸袁,主類在運(yùn)行過程中如果使用到其它類钥顽,會逐步加載這些類。jar包或war包里的類不是一次性全部加載的靠汁,是使用到時才加載蜂大。請看下面例子:

public class TestDynamicLoad {
    static {
        System.out.println("*************加載主啟動類************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*******加載測試********");
        B b = null;//B不會加載,除非這里執(zhí)行new B();
    }
}

class A{
    static {
        System.out.println("*******加載A類********");
    }

    public A() {
        System.out.println("*******初始化A類********");
    }
}

class B{
    static {
        System.out.println("*******加載B類********");
    }

    public B() {
        System.out.println("*******初始化B類********");
    }
}
運(yùn)行結(jié)果:
*************加載主啟動類************
*******加載A類********
*******初始化A類********
*******加載測試********

類加載器和雙親委派機(jī)制

上面的類加載過程主要是通過類加載器來實(shí)現(xiàn)的蝶怔,Java里有如下幾種類加載器:

  1. Bootstrp loader
    Bootstrp加載器是用C++語言寫的奶浦,它是在Java虛擬機(jī)啟動后初始化的,它主要負(fù)責(zé)加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類踢星。

  2. ExtClassLoader
    Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設(shè)置為Bootstrp loader.ExtClassLoader是用Java寫的澳叉,具體來說就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext沐悦,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類庫成洗。

  3. AppClassLoader
    Bootstrp loader加載完ExtClassLoader后,就會加載AppClassLoader,并且將AppClassLoader的父加載器指定為 ExtClassLoader藏否。AppClassLoader也是用Java寫成的瓶殃,它的實(shí)現(xiàn)類是 sun.misc.Launcher$AppClassLoader,另外我們知道ClassLoader中有個getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要負(fù)責(zé)加載classpath所指定的位置的類或者是jar文檔副签,它也是Java程序默認(rèn)的類加載器遥椿。

類加載器初始化過程:

參見類運(yùn)行加載全過程圖可知其中會創(chuàng)建JVM啟動器實(shí)例sun.misc.Launcher。 sun.misc.Launcher初始化使用了單例模式設(shè)計(jì)淆储,保證一個JVM虛擬機(jī)內(nèi)只有一個 sun.misc.Launcher實(shí)例冠场。 在Launcher構(gòu)造方法內(nèi)部,其創(chuàng)建了兩個類加載器遏考,分別是 sun.misc.Launcher.ExtClassLoader(擴(kuò)展類加載器)和sun.misc.Launcher.AppClassLoader(應(yīng) 用類加載器)慈鸠。 JVM默認(rèn)使用Launcher的getClassLoader()方法返回的類加載器AppClassLoader的實(shí)例加載我們 的應(yīng)用程序。

jdk源代碼如下:

//Launcher的構(gòu)造方法
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
    //構(gòu)造擴(kuò)展類加載器灌具,在構(gòu)造的過程中將其父加載器設(shè)置為null
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader",      var10);
        }
    try {
        //構(gòu)造應(yīng)用類加載器青团,在構(gòu)造的過程中將其父加載器設(shè)置為ExtClassLoader,
        //Launcher的loader屬性值是AppClassLoader咖楣,我們一般都是用這個類加載器來加載我們自己寫的應(yīng)用程序
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        Thread.currentThread().setContextClassLoader(this.loader);

雙親委派機(jī)制

前面說了督笆,java中有三個類加載器,問題就來了诱贿,碰到一個類需要加載時娃肿,它們之間是如何協(xié)調(diào)工作的咕缎,即java是如何區(qū)分一個類該由哪個類加載器來完成呢。 在這里java采用了委托模型機(jī)制料扰,這個機(jī)制簡單來講凭豪,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入晒杈,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類

下面舉一個例子來說明嫂伞,為了更好的理解,先弄清楚幾行代碼:

Public class Test{
 
    Public static void main(String[] arg){
 
      ClassLoader c  = Test.class.getClassLoader();  //獲取Test類的類加載器
 
        System.out.println(c); 
 
      ClassLoader c1 = c.getParent();  //獲取c這個類加載器的父類加載器
 
        System.out.println(c1);
 
      ClassLoader c2 = c1.getParent();//獲取c1這個類加載器的父類加載器
 
        System.out.println(c2);
 
  }
 
}

結(jié)果:
……AppClassLoader……
 
……ExtClassLoader……
 
Null

可以看出Test是由AppClassLoader加載器加載的拯钻,AppClassLoaderParent 加載器是 ExtClassLoader,但是ExtClassLoaderParentnull 是怎么回事呵帖努,朋友們留意的話,前面有提到Bootstrap Loader是用C++語言寫的粪般,依java的觀點(diǎn)來看拼余,邏輯上并不存在Bootstrap Loader的類實(shí)體,所以在java程序代碼里試圖打印出其內(nèi)容時亩歹,我們就會看到輸出為null匙监。

我們來看下應(yīng)用程序類加載器AppClassLoader加載類的雙親委派機(jī)制源碼,AppClassLoader 的loadClass方法最終會調(diào)用其父類ClassLoader的loadClass方法小作,該方法的大體邏輯如下:

  1. 首先舅柜,檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了躲惰,就不需要再加載致份,直接 返回。

  2. 如果此類沒有加載過础拨,那么氮块,再判斷一下是否有父加載器;如果有父加載器诡宗,則由父加 載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加 載滔蝉。

  3. 如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當(dāng)前類加載器的 findClass方法來完成類加載塔沃。

源代碼如下:

ClassLoader.java

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
           // 檢查當(dāng)前類加載器是否已經(jīng)加載了該類
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { //如果當(dāng)前加載器父加載器不為空則委托父加載器加載該類
                        c = parent.loadClass(name, false);
                    } else {//如果當(dāng)前加載器父加載器為空則委托引導(dǎo)類加載器加載該類
                        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();
                    //都會調(diào)用URLClassLoader的findClass方法在加載器的類路徑里查找并加載該類
                    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;
        }
    }

URLClassLoader.java

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        //匹配被加載類的路徑和當(dāng)前類加載器的加載路徑蝠引,看能否匹配到
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //如果能匹配到,就進(jìn)行真正的類加載蛀柴,
                                //就會執(zhí)行前面說的類加載的幾個階段
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

那么為什么要設(shè)計(jì)雙親委派機(jī)制螃概?

主要有以下2點(diǎn)原因:

  1. 沙箱安全機(jī)制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
  2. 避免類的重復(fù)加載:當(dāng)父親已經(jīng)加載了該類時鸽疾,就沒有必要子ClassLoader再加載一 次吊洼,保證被加載類的唯一性

Tomcat打破雙親委派機(jī)制

以Tomcat類加載為例,Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行制肮?

我們思考一下:Tomcat是個web容器冒窍, 那么它要解決什么問題:

  1. 一個web容器可能需要部署兩個應(yīng)用程序递沪,不同的應(yīng)用程序可能會依賴同一個第三方類庫的 不同版本,不能要求同一個類庫在同一個服務(wù)器只有一份综液,因此要保證每個應(yīng)用程序的類庫都是 獨(dú)立的款慨,保證相互隔離。

  2. 部署在同一個web容器中相同的類庫相同的版本可以共享谬莹。否則樱调,如果服務(wù)器有10個應(yīng)用程 序,那么要有10份相同的類庫加載進(jìn)虛擬機(jī)届良。

  3. web容器也有自己依賴的類庫,不能與應(yīng)用程序的類庫混淆圣猎∈亢基于安全考慮,應(yīng)該讓容器的 類庫和程序的類庫隔離開來送悔。

  4. web容器要支持jsp的修改慢显,我們知道,jsp 文件最終也是要編譯成class文件才能在虛擬機(jī)中 運(yùn)行欠啤,但程序運(yùn)行后修改jsp已經(jīng)是司空見慣的事情荚藻, web容器需要支持 jsp 修改后不用重啟。

再看看我們的問題:Tomcat 如果使用默認(rèn)的雙親委派類加載機(jī)制行不行洁段?

答案是不行的应狱。為什么?

第一個問題祠丝,如果使用默認(rèn)的類加載器機(jī)制疾呻,那么是無法加載兩個相同類庫的不同版本的,默認(rèn) 的類加器是不管你是什么版本的写半,只在乎你的全限定類名岸蜗,并且只有一份。

第二個問題叠蝇,默認(rèn)的類加載器是能夠?qū)崿F(xiàn)的璃岳,因?yàn)樗穆氊?zé)就是保證唯一性

第三個問題和第一個問題一樣悔捶。

我們再看第四個問題铃慷,我們想我們要怎么實(shí)現(xiàn)jsp文件的熱加載,jsp 文件其實(shí)也就是class文 件蜕该,那么如果修改了枚冗,但類名還是一樣,類加載器會直接取方法區(qū)中已經(jīng)存在的蛇损,修改后的jsp 是不會重新加載的赁温。那么怎么辦呢坛怪?我們可以直接卸載掉這jsp文件的類加載器,所以你應(yīng)該想 到了股囊,每個jsp文件對應(yīng)一個唯一的類加載器袜匿,當(dāng)一個jsp文件修改了,就直接卸載這個jsp類加載 器稚疹。重新創(chuàng)建類加載器居灯,重新加載jsp文件。

Tomcat自定義加載器詳解

tomcat類加載器.png

tomcat的幾個主要類加載器:

  • commonLoader:Tomcat最基本的類加載器内狗,加載路徑中的class可以被Tomcat容 器本身以及各個Webapp訪問怪嫌;

  • catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對于Webapp不 可見柳沙;

  • sharedLoader:各個Webapp共享的類加載器岩灭,加載路徑中的class對于所有 Webapp可見,但是對于Tomcat容器不可見赂鲤;

  • WebappClassLoader:各個Webapp私有的類加載器噪径,加載路徑中的class只對當(dāng)前 Webapp可見,比如加載war包里相關(guān)的類数初,每個war包應(yīng)用都有自己的WebappClassLoader找爱,實(shí)現(xiàn)相互隔離,比如不同war包應(yīng)用引入了不同的spring版本泡孩, 這樣實(shí)現(xiàn)就能加載各自的spring版本车摄;

從圖中的委派關(guān)系中可以看出:

CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用, 從而實(shí)現(xiàn)了公有類庫的共用仑鸥,而CatalinaClassLoader和SharedClassLoader自己能加載的類則 與對方相互隔離练般。

WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader 實(shí)例之間相互隔離锈候。

而JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個.Class文件薄料,它出現(xiàn)的目的 就是為了被丟棄:當(dāng)Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實(shí)例泵琳, 并通過再建立一個新的Jsp類加載器來實(shí)現(xiàn)JSP文件的熱加載功能摄职。

tomcat 這種類加載機(jī)制違背了java 推薦的雙親委派模型了嗎?答案是:違背了获列。

很顯然谷市,tomcat 不是這樣實(shí)現(xiàn),tomcat 為了實(shí)現(xiàn)隔離性击孩,沒有遵守這個約定迫悠,每個 webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器巩梢,打破了雙親委 派機(jī)制创泄。


iu.jpeg

關(guān)于類加載機(jī)制就分析到這里了艺玲,原創(chuàng)不易,覺得寫得不錯的話就點(diǎn)點(diǎn)贊關(guān)注關(guān)注唄鞠抑,我的微信公眾號:java時光

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饭聚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搁拙,更是在濱河造成了極大的恐慌秒梳,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箕速,死亡現(xiàn)場離奇詭異酪碘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盐茎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門兴垦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人庭呜,你說我怎么就攤上這事∠溃” “怎么了募谎?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阴汇。 經(jīng)常有香客問我数冬,道長,這世上最難降的妖魔是什么搀庶? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任拐纱,我火速辦了婚禮,結(jié)果婚禮上哥倔,老公的妹妹穿的比我還像新娘秸架。我一直安慰自己,他們只是感情好咆蒿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布东抹。 她就那樣靜靜地躺著,像睡著了一般沃测。 火紅的嫁衣襯著肌膚如雪缭黔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天蒂破,我揣著相機(jī)與錄音馏谨,去河邊找鬼。 笑死附迷,一個胖子當(dāng)著我的面吹牛惧互,可吹牛的內(nèi)容都是我干的哎媚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壹哺,長吁一口氣:“原來是場噩夢啊……” “哼抄伍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起管宵,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤截珍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后箩朴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岗喉,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年炸庞,在試婚紗的時候發(fā)現(xiàn)自己被綠了钱床。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡埠居,死狀恐怖查牌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情滥壕,我是刑警寧澤纸颜,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站绎橘,受9級特大地震影響胁孙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜称鳞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一涮较、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冈止,春花似錦狂票、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至怨咪,卻和暖如春屋剑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诗眨。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工唉匾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓巍膘,卻偏偏與公主長得像厂财,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子峡懈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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