JVM——深入理解Java類加載器(ClassLoader)

類加載的機制的層次結(jié)構(gòu)

每個編寫的”.java”拓展名類文件都存儲著需要執(zhí)行的程序邏輯吴裤,這些”.java”文件經(jīng)過Java編譯器編譯成拓展名為”.class”的文件恨溜,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機指令,當(dāng)需要使用某個類時摆寄,虛擬機將會加載它的”.class”文件垃环,并創(chuàng)建對應(yīng)的class對象,將class文件加載到虛擬機的內(nèi)存异赫,這個過程稱為類加載椅挣,這里我們需要了解一下類加載的過程头岔,如下:


  • 加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個Class對象

  • 驗證:目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機要求鼠证,不會危害虛擬機自身安全峡竣。主要包括四種驗證,文件格式驗證量九,元數(shù)據(jù)驗證适掰,字節(jié)碼驗證,符號引用驗證荠列。

  • 準(zhǔn)備:為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值即0(如static int i=5;這里只將i初始化為0类浪,至于5的值將在初始化時賦值),這里不包含用final修飾的static肌似,因為final在編譯的時候就會分配了费就,注意這里不會為實例變量分配初始化,類變量會分配在方法區(qū)中锈嫩,而實例變量是會隨著對象一起分配到Java堆中受楼。

  • 解析:主要將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標(biāo)呼寸,可以是任何字面量艳汽,而直接引用就是直接指向目標(biāo)的指針、相對偏移量或一個間接定位到目標(biāo)的句柄对雪。有類或接口的解析河狐,字段解析,類方法解析瑟捣,接口方法解析(這里涉及到字節(jié)碼變量的引用馋艺,如需更詳細(xì)了解,可參考《深入Java虛擬機》)迈套。

  • 初始化:類加載最后階段捐祠,若該類具有超類衷敌,則對其進(jìn)行初始化苔埋,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化成員變量(如前面只初始化了默認(rèn)值的static變量將會在這個階段賦值,成員變量也將被初始化)馋辈。

這便是類加載的5個過程贵白,而類加載器的任務(wù)是根據(jù)一個類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM中率拒,然后轉(zhuǎn)換為一個與目標(biāo)類對應(yīng)的java.lang.Class對象實例,在虛擬機提供了3種類加載器禁荒,引導(dǎo)(Bootstrap)類加載器猬膨、擴展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器)

ClassLoader加載機制如下:

啟動(Bootstrap)類加載器

啟動類加載器主要加載的是JVM自身需要的類呛伴,這個類加載使用C++語言實現(xiàn)的勃痴,是虛擬機自身的一部分谒所,它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機是按照文件名識別加載jar包的召耘,如rt.jar百炬,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮污它,Bootstrap啟動類加載器只加載包名為java剖踊、javax、sun等開頭的類)衫贬。

擴展(Extension)類加載器

擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader類德澈,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類固惯,它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫梆造,開發(fā)者可以直接使用標(biāo)準(zhǔn)擴展類加載器。

//ExtClassLoader類中獲取路徑的代碼
private static File[] getExtDirs() {
     //加載<JAVA_HOME>/lib/ext目錄中的類庫
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

系統(tǒng)(System)類加載器

也稱應(yīng)用程序加載器是指 Sun公司實現(xiàn)的sun.misc.Launcher$AppClassLoader葬毫。它負(fù)責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類庫镇辉,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器贴捡,一般情況下該類加載是程序中默認(rèn)的類加載器忽肛,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。

在Java的日常應(yīng)用程序開發(fā)中烂斋,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的屹逛,在必要時,我們還可以自定義類加載器汛骂,需要注意的是罕模,Java虛擬機對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象帘瞭,而且加載某個類的class文件時淑掌,Java虛擬機采用的是雙親委派模式即把請求交由父類處理,它一種任務(wù)委派模式蝶念,下面我們進(jìn)一步了解它锋拖。

理解雙親委派模式

雙親委派模式工作原理

雙親委派模式要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器祸轮,請注意雙親委派模式中的父子關(guān)系并非通常所說的類繼承關(guān)系,而是采用組合關(guān)系來復(fù)用父類加載器的相關(guān)代碼侥钳,類加載器間的關(guān)系如下:


雙親委派模式是在Java 1.2后引入的适袜,其工作原理的是,如果一個類加載器收到了類加載請求舷夺,它并不會自己先去加載苦酱,而是把這個請求委托給父類的加載器去執(zhí)行售貌,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托疫萤,依次遞歸颂跨,請求最終將到達(dá)頂層的啟動類加載器,如果父類加載器可以完成類加載任務(wù)扯饶,就成功返回恒削,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載尾序,這就是雙親委派模式钓丰。

雙親委派模式優(yōu)勢

類加載器的雙親委托模型好處:

  • 1、可以確保Java核心庫的類型安全:所有的Java應(yīng)用都至少會引用java.lang.Object類每币,也就是在運行期java.lang.Object類會加載到Java虛擬機中携丁,如果這個加載過程是由Java自己的類加載器所完成,那么就很可能會在JVM中存在多個版本的java.lang.Object類兰怠,而且這些類還不兼容梦鉴,并且互相不可見(正是命名空間在發(fā)揮作用),借助于雙親委托機制揭保,Java核心類庫中的類的加載工作肥橙,都是由啟動類加載器來統(tǒng)一完成加載工作,從而確保了Java應(yīng)用使用的都是同一個版本的Java核心類庫掖举,它們之間是相互兼容的快骗。

  • 2、可以確保Java核心類庫所提供的類塔次,不會被自定義的類所替代方篮。

  • 3、不同的類加載器可以為相同名稱(binary name)的類創(chuàng)建額外的命名空間励负,相同名稱的類可以并存在Java虛擬機中藕溅,只需要不同的類加載器來加載它們即可,不同類加載器所加載的類之間是不兼容的继榆,這就相當(dāng)于在Java虛擬機內(nèi)部創(chuàng)建了一個又一個項目隔離的Java類空間巾表,這類技術(shù)在很多框架中都得到了實際應(yīng)用。

  • 4略吨、采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系集币,通過這種層級關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時翠忠,就沒有必要子ClassLoader再加載一次鞠苟。

Bootstrap啟動類加載器作用:

在運行期,一個Java類是由該類的完全限定名(binary name,二進(jìn)制名)和用于加載該類的定義類加載器(defining loader)所共同決定的当娱,如果同樣名字(即相同的全限定名)的類是由兩個不同的加載器所加載吃既,那么這些類就是不同的,即便.class文件的字節(jié)碼完全一樣跨细,并且從相同的位置加載亦是如此鹦倚。

在Oracle的Hotspot實現(xiàn)中,系統(tǒng)屬性sun.boot.class.path如果修改錯了冀惭,則運行出錯震叙。

內(nèi)建于JVM中的啟動類加載器會加載java.lang.ClassLoader以及其他的Java平臺類,當(dāng)JVM啟動時云头,一塊特殊的機器碼會運行捐友,它會加載擴展類加載器與系統(tǒng)類加載器,這塊特殊的機器碼叫做啟動類加載器(Bootstrap)溃槐,啟動類加載器并不是Java類匣砖,而其他的加載器是Java類,啟動類加載器是特于平臺的機器指令昏滴,它負(fù)責(zé)開啟整個加載過程

  • 所有類加載器(除了啟動類加載器)都被實現(xiàn)為Java類猴鲫,不過總歸要有一個組件來加載第一個Java類加載器,從而讓整個加載過程能夠順利進(jìn)行下去谣殊,加載第一個純Java類加載器就是啟動類加載器的職責(zé)

  • 啟動類加載器還會負(fù)責(zé)加載供JRE正常運行所需要的基本組件拂共,即rt包下的類,包括java.util和java.lang包等

下面代碼可以看到各類加載器的加載路徑:

public class MyTest22 {

    public static void main(String[] args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));

        System.out.println(ClassLoader.class.getClassLoader());
        System.out.println(Launcher.class.getClassLoader());
    }
}

下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現(xiàn)姻几,它們類圖關(guān)系如下


從圖可以看出頂層的類加載器是ClassLoader類宜狐,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)蛇捌,這里我們主要介紹ClassLoader中幾個比較重要的方法抚恒。

  • loadClass(String name, boolean resolve)
    該方法加載指定名稱(包括包名)的二進(jìn)制類型,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調(diào)用該方法络拌,loadClass()方法是ClassLoader類自己實現(xiàn)的俭驮,該方法中的邏輯就是雙親委派模式的實現(xiàn),其源碼如下春贸,loadClass(String name, boolean resolve)是一個重載方法混萝,resolve參數(shù)代表是否生成class對象的同時進(jìn)行解析相關(guān)操作。:
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //  // 先從緩存查找該class對象萍恕,找到就不用重新加載
        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();
                // 如果都沒有找到允粤,則通過自定義實現(xiàn)的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) {
            //是否需要在加載時進(jìn)行解析
            resolveClass(c);
        }
        return c;
    }
}

正如loadClass方法所展示的厘熟,當(dāng)類加載請求到來時屯蹦,先從緩存中查找該類對象,如果存在直接返回绳姨,如果不存在則交給該類加載器的父加載器去加載,倘若沒有父加載器則交給頂級啟動類加載器去加載阔挠,最后倘若仍沒有找到飘庄,則使用findClass()方法去加載(關(guān)于findClass()稍后會進(jìn)一步介紹)。從loadClass實現(xiàn)也可以知道如果不想重新定義加載類的規(guī)則购撼,也沒有復(fù)雜的邏輯跪削,只想在運行時加載自己指定的類,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className")迂求,這樣就可以直接調(diào)用ClassLoader的loadClass方法獲取到class對象碾盐。

  • findClass(String name)
    在JDK1.2之前,在自定義類加載時揩局,總會去繼承ClassLoader類并重寫loadClass方法毫玖,從而實現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法凌盯,而是建議把自定義的類加載邏輯寫在findClass()方法中付枫,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的驰怎,當(dāng)loadClass()方法中父加載器加載失敗后阐滩,則會調(diào)用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式县忌。需要注意的是ClassLoader類中并沒有實現(xiàn)findClass()方法的具體代碼邏輯掂榔,取而代之的是拋出ClassNotFoundException異常,同時應(yīng)該知道的是findClass方法通常是和defineClass方法一起使用的(稍后會分析)症杏,ClassLoader類中findClass()方法源碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
        //直接拋出異常
    throw new ClassNotFoundException(name);
}
  • defineClass(byte[] b, int off, int len)
    defineClass()方法是用來將byte字節(jié)流解析成JVM能夠識別的Class對象(ClassLoader中已實現(xiàn)該方法邏輯)装获,通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象鸳慈,如通過網(wǎng)絡(luò)接收一個類的字節(jié)碼饱溢,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對應(yīng)的Class對象,defineClass()方法通常與findClass()方法一起使用走芋,一般情況下绩郎,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則翁逞,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流肋杖,然后調(diào)用defineClass()方法生成類的Class對象,簡單例子如下:
public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 獲取類的字節(jié)數(shù)組
        byte[] data = this.loadClassData(className);
        if(data == null){
            throw new ClassNotFoundException();
        }
        //使用defineClass生成class對象
        return this.defineClass(className,data,0,data.length);
    }
}

需要注意的是挖函,如果直接調(diào)用defineClass()方法生成類的Class對象状植,這個類的Class對象并沒有解析(也可以理解為鏈接階段,畢竟解析是鏈接的最后一步),其解析操作需要等待初始化階段進(jìn)行津畸。

  • resolveClass(Class<?> c)
    使用該方法可以使用類的Class對象創(chuàng)建完成也同時被解析振定。前面我們說鏈接階段主要是對字節(jié)碼進(jìn)行驗證,為類變量分配內(nèi)存并設(shè)置初始值同時將字節(jié)碼文件中的符號引用轉(zhuǎn)換為直接引用肉拓。

上述4個方法是ClassLoader類中的比較重要的方法后频,也是我們可能會經(jīng)常用到的方法。接看SercureClassLoader擴展了 ClassLoader暖途,新增了幾個與使用相關(guān)的代碼源(對代碼源的位置及其證書的驗證)和權(quán)限定義類驗證(主要指對class源碼的訪問權(quán)限)的方法卑惜,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關(guān)聯(lián)驻售,前面說過露久,ClassLoader是一個抽象類,很多方法是空的沒有實現(xiàn)欺栗,比如 findClass()毫痕、findResource()等。而URLClassLoader這個實現(xiàn)類為這些方法提供了具體的實現(xiàn)纸巷,并新增了URLClassPath類協(xié)助取得Class字節(jié)碼流等功能镇草,在編寫自定義類加載器時,如果沒有太過于復(fù)雜的需求瘤旨,可以直接繼承URLClassLoader類梯啤,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔存哲,下面是URLClassLoader的類圖(利用IDEA生成的類圖)


  • 從類圖結(jié)構(gòu)看出URLClassLoader中存在一個URLClassPath類因宇,通過這個類就可以找到要加載的字節(jié)碼流,也就是說URLClassPath類負(fù)責(zé)找到要加載的字節(jié)碼祟偷,再讀取成字節(jié)流察滑,最后通過defineClass()方法創(chuàng)建類的Class對象。

  • 從URLClassLoader類的結(jié)構(gòu)圖可以看出其構(gòu)造方法都有一個必須傳遞的參數(shù)URL[]修肠,該參數(shù)的元素是代表字節(jié)碼文件的路徑,換句話說在創(chuàng)建URLClassLoader對象時必須要指定這個類加載器的到那個目錄下找class文件贺辰。

  • 同時也應(yīng)該注意URL[]也是URLClassPath類的必傳參數(shù),在創(chuàng)建URLClassPath對象時嵌施,會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包饲化,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認(rèn)Loader類去加載相應(yīng)路徑下的class文件,而當(dāng)JVM調(diào)用findClass()方法時吗伤,就由這3個加載器中的一個將class文件的字節(jié)碼流加載到內(nèi)存中吃靠,最后利用字節(jié)碼流創(chuàng)建類的class對象。

  • 請記住足淆,如果我們在定義類加載器時選擇繼承ClassLoader類而非URLClassLoader巢块,必須手動編寫findclass()方法的加載邏輯以及獲取字節(jié)碼流的邏輯礁阁。

了解完URLClassLoader后接著看看剩余的兩個類加載器,即拓展類加載器ExtClassLoader和系統(tǒng)類加載器AppClassLoader族奢,這兩個類都繼承自URLClassLoader姥闭,是sun.misc.Launcher的靜態(tài)內(nèi)部類。sun.misc.Launcher主要被系統(tǒng)用于啟動主應(yīng)用程序歹鱼,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher創(chuàng)建的泣栈,其類主要類結(jié)構(gòu)如下:

它們間的關(guān)系正如前面所闡述的那樣,同時我們發(fā)現(xiàn)ExtClassLoader并沒有重寫loadClass()方法弥姻,這足矣說明其遵循雙親委派模式,而AppClassLoader重載了loadCass()方法掺涛,但最終調(diào)用的還是父類loadClass()方法庭敦,因此依然遵守雙親委派模式,重載方法源碼如下:

/**
 * Override loadClass 方法薪缆,新增包權(quán)限檢測功能 
 */
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
    int var3 = var1.lastIndexOf(46);
    if (var3 != -1) {
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var4.checkPackageAccess(var1.substring(0, var3));
        }
    }

    if (this.ucp.knownToNotExist(var1)) {
        Class var5 = this.findLoadedClass(var1);
        if (var5 != null) {
            if (var2) {
                this.resolveClass(var5);
            }

            return var5;
        } else {
            throw new ClassNotFoundException(var1);
        }
    } else {
        //依然調(diào)用父類的方法
        return super.loadClass(var1, var2);
    }
}

其實無論是ExtClassLoader還是AppClassLoader都繼承URLClassLoader類秧廉,因此它們都遵守雙親委托模型,這點是毋庸置疑的拣帽。

到此我們對ClassLoader疼电、URLClassLoader、ExtClassLoader减拭、AppClassLoader以及Launcher類間的關(guān)系有了比較清晰的了解蔽豺,同時對一些主要的方法也有一定的認(rèn)識,這里并沒有對這些類的源碼進(jìn)行詳細(xì)的分析拧粪,畢竟沒有那個必要修陡,因為我們主要弄得類與類間的關(guān)系和常用的方法同時搞清楚雙親委托模式的實現(xiàn)過程,為編寫自定義類加載器做鋪墊就足夠了可霎。前面出現(xiàn)了很多父類加載器的說法魄鸦,但每個類加載器的父類到底是誰,一直沒有闡明癣朗,下面我們就通過代碼驗證的方式來闡明這答案拾因。

類加載器間的關(guān)系

我們進(jìn)一步了解類加載器間的關(guān)系(并非指繼承關(guān)系),主要可以分為以下4點:

  • 啟動類加載器旷余,由C++實現(xiàn)绢记,沒有父類。
  • 拓展類加載器(ExtClassLoader)荣暮,由Java語言實現(xiàn)庭惜,父類加載器為null
  • 系統(tǒng)類加載器(AppClassLoader),由Java語言實現(xiàn)穗酥,父類加載器為ExtClassLoader
  • 自定義類加載器护赊,父類加載器肯定為AppClassLoader惠遏。

下面我們通過程序來驗證上述闡述的觀點

public class MyClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private final String fileExtension = ".class";

    public MyClassLoader(String classLoaderName){
        super();//將系統(tǒng)類加載器作為該類加載器的父加載器
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader parent,String classLoaderName){
        super(parent);//顯示指定該類加載器的父加載器
        this.classLoaderName = classLoaderName;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "MyTest15{" +
                "classLoaderName='" + classLoaderName + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked:"+className);
        System.out.println("class loader name:"+this.classLoaderName);
        byte[] data = this.loadClassData(className);
        if(data == null){
            throw new ClassNotFoundException();
        }
        return this.defineClass(className,data,0,data.length);
    }

    private byte[] loadClassData(String className){
        InputStream inputStream = null;
        byte[] data = null;
        ByteArrayOutputStream bos = null;

        try {
            this.path = path.replace(".","/");
            this.classLoaderName = classLoaderName.replace(".","/");
            inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
            bos = new ByteArrayOutputStream();
            int ch;
            while((ch = inputStream.read()) != -1){
                bos.write(ch);
            }
            data = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }


    public static void main(String[] args) throws Exception {
        MyClassLoader loader1 = new MyClassLoader("loader1");
        System.out.println("自定義類加載器的父加載器: "+loader1.getParent());
        System.out.println("系統(tǒng)默認(rèn)的AppClassLoader: "+ClassLoader.getSystemClassLoader());
        System.out.println("AppClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent());
        System.out.println("ExtClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent().getParent());

        System.out.println("------------------");

        MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
        myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz1:" + clazz1);
        Object object1 = clazz1.newInstance();
        System.out.println(object1);
        System.out.println(myClassLoader1.getClass().getClassLoader());

        System.out.println("------------------");

        MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
        myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz2:" + clazz2);
        Object object2 = clazz2.newInstance();
        System.out.println(object2);

        System.out.println("------------------");

        MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
        myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz3:" + clazz3);
        Object object3 = clazz3.newInstance();
        System.out.println(object3);
    }
}

代碼中,我們自定義了一個MyClassLoader骏啰,這里我們繼承了ClassLoader而非URLClassLoader,因此需要自己編寫findClass()方法邏輯以及加載字節(jié)碼的邏輯节吮,接著在main方法中,通過ClassLoader.getSystemClassLoader()獲取到系統(tǒng)默認(rèn)類加載器判耕,通過獲取其父類加載器及其父父類加載器透绩,同時還獲取了自定義類加載器的父類加載器,最終輸出結(jié)果正如我們所預(yù)料的,AppClassLoader的父類加載器為ExtClassLoader壁熄,而ExtClassLoader沒有父類加載器帚豪。如果我們實現(xiàn)自己的類加載器,它的父加載器都只會是AppClassLoader草丧。這里我們不妨看看Lancher的構(gòu)造器源碼:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 首先創(chuàng)建擴展類加載器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //再創(chuàng)建AppClassLoader并把擴展類加載器作為父加載器傳遞給AppClassLoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    //設(shè)置線程上下文類加載器狸臣,稍后分析
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
            } catch (InstantiationException var6) {
            } catch (ClassNotFoundException var7) {
            } catch (ClassCastException var8) {
            }
        } else {
            var3 = new SecurityManager();
        }

        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }
        System.setSecurityManager(var3);
    }
}

顯然Lancher初始化時首先會創(chuàng)建ExtClassLoader類加載器,然后再創(chuàng)建AppClassLoader并把ExtClassLoader傳遞給它作為父類加載器昌执,這里還把AppClassLoader默認(rèn)設(shè)置為線程上下文類加載器烛亦,關(guān)于線程上下文類加載器稍后會分析。那ExtClassLoader類加載器為什么是null呢懂拾?看下面的源碼創(chuàng)建過程就明白煤禽,在創(chuàng)建ExtClassLoader強制設(shè)置了其父加載器為null。

// 首先創(chuàng)建擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();

static class ExtClassLoader extends URLClassLoader {
    private static volatile Launcher.ExtClassLoader instance;

    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        if (instance == null) {
            Class var0 = Launcher.ExtClassLoader.class;
            synchronized(Launcher.ExtClassLoader.class) {
                if (instance == null) {
                    instance = createExtClassLoader();
                }
            }
        }

        return instance;
    }

    public ExtClassLoader(File[] var1) throws IOException {
        //調(diào)用父類構(gòu)造URLClassLoader傳遞null作為parent
        super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
        SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
    }
}

public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    acc = AccessController.getContext();
    ucp = new URLClassPath(urls, factory, acc);
}

顯然ExtClassLoader的父類為null岖赋,而AppClassLoader的父加載器為ExtClassLoader檬果,所有自定義的類加載器其父加載器只會是AppClassLoader,注意這里所指的父類并不是Java繼承關(guān)系中的那種父子關(guān)系贾节。

類與類加載器

在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件

  • 類的全限定名必須一致汁汗,包括包名。
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同栗涂。

也就是說知牌,在JVM中,即使這個兩個類對象(class對象)來源同一個Class文件斤程,被同一個虛擬機所加載角寸,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的忿墅,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間扁藕,所以加載的class對象也會存在不同的類名空間中,但前提是覆寫loadclass方法疚脐,從前面雙親委派模式對loadClass()方法的源碼分析中可以知亿柑,在方法第一步會通過Class<?> c = findLoadedClass(name);從緩存查找,類名完整名稱相同則不會再次被加載棍弄,因此我們必須繞過緩存查詢才能重新加載class對象望薄。當(dāng)然也可直接調(diào)用findClass()方法疟游,這樣也避免從緩存查找。如果不從緩存查詢相同完全類名的class對象痕支,那么只有ClassLoader的實例對象不同颁虐,同一字節(jié)碼文件創(chuàng)建的class對象自然也不會相同。

了解class文件的顯示加載與隱式加載的概念

所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式卧须,顯示加載指的是在代碼中通過調(diào)用ClassLoader加載class對象另绩,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對象。
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象花嘶,而是通過虛擬機自動加載到內(nèi)存中笋籽,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象椭员,此時額外引用的類將通過JVM自動加載到內(nèi)存中干签。在日常開發(fā)以上兩種方式一般會混合使用,這里我們知道有這么回事即可拆撼。

編寫自己的類加載器

通過前面的分析可知,實現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader喘沿,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯闸度,繼承URLClassLoader則可以省去編寫findClass()方法以及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。那么編寫自定義類加載器的意義何在呢蚜印?

  • 當(dāng)class文件不在ClassPath路徑下莺禁,默認(rèn)系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實現(xiàn)一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象窄赋。
  • 當(dāng)一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進(jìn)行相應(yīng)的加密操作時哟冬,需要先對class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實現(xiàn)相應(yīng)的邏輯忆绰。
  • 當(dāng)需要實現(xiàn)熱部署功能時(一個class文件通過不同的類加載器產(chǎn)生不同class對象從而實現(xiàn)熱部署功能)浩峡,需要實現(xiàn)自定義ClassLoader的邏輯。

繼承ClassLoader實現(xiàn)自定義類加載器

public class MyClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private final String fileExtension = ".class";

    public MyClassLoader(String classLoaderName){
        super();//將系統(tǒng)類加載器作為該類加載器的父加載器
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader parent,String classLoaderName){
        super(parent);//顯示指定該類加載器的父加載器
        this.classLoaderName = classLoaderName;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "MyTest15{" +
                "classLoaderName='" + classLoaderName + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked:"+className);
        System.out.println("class loader name:"+this.classLoaderName);
        byte[] data = this.loadClassData(className);
        if(data == null){
            throw new ClassNotFoundException();
        }
        return this.defineClass(className,data,0,data.length);
    }

    private byte[] loadClassData(String className){
        InputStream inputStream = null;
        byte[] data = null;
        ByteArrayOutputStream bos = null;

        try {
            this.path = path.replace(".","/");
            this.classLoaderName = classLoaderName.replace(".","/");
            inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
            bos = new ByteArrayOutputStream();
            int ch;
            while((ch = inputStream.read()) != -1){
                bos.write(ch);
            }
            data = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }


    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
        myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz1:" + clazz1);
        Object object1 = clazz1.newInstance();
        System.out.println(object1);
        System.out.println(myClassLoader1.getClass().getClassLoader());

        System.out.println("------------------");

        MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
        myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz2:" + clazz2);
        Object object2 = clazz2.newInstance();
        System.out.println(object2);

        System.out.println("------------------");

        MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
        myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
        Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
        System.out.println("clazz3:" + clazz3);
        Object object3 = clazz3.newInstance();
        System.out.println(object3);
    }
}

顯然我們通過loadClassData()方法找到class文件并轉(zhuǎn)換為字節(jié)流错敢,并重寫findClass()方法翰灾,利用defineClass()方法創(chuàng)建了類的class對象。在main方法中調(diào)用了setPath()方法加載指定路徑下的class文件稚茅,由于啟動類加載器纸淮、拓展類加載器以及系統(tǒng)類加載器都無法在其路徑下找到該類,因此最終將有自定義類加載器加載亚享,即調(diào)用findClass()方法進(jìn)行加載咽块。如果繼承URLClassLoader實現(xiàn),那代碼就更簡潔了欺税,如下:

繼承URLClassLoader現(xiàn)自定義類加載器

public class MyURLClassLoader extends URLClassLoader {


    public MyURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public MyURLClassLoader(URL[] urls) {
        super(urls);
    }

    public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    public static void main(String[] args) throws MalformedURLException {
        String rootDir="E:/gradleproject/jvm_study/build/classes/java/main/";
        //創(chuàng)建自定義文件類加載器
        File file = new File(rootDir);
        //File to URI
        URI uri=file.toURI();
        URL[] urls = {uri.toURL()};

        MyURLClassLoader loader = new MyURLClassLoader(urls);

        try {
            //加載指定的class文件
            Class<?> object =loader.loadClass("com.yibo.jvm.classloader.MyTest1");
            System.out.println(object.newInstance().toString());
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

非常簡潔除了需要重寫構(gòu)造器外無需編寫findClass()方法及其class文件的字節(jié)流轉(zhuǎn)換邏輯侈沪。

自定義網(wǎng)絡(luò)類加載器

自定義網(wǎng)絡(luò)類加載器揭璃,主要用于讀取通過網(wǎng)絡(luò)傳遞的class文件(在這里我們省略class文件的解密過程),并將其轉(zhuǎn)換成字節(jié)流生成對應(yīng)的class對象峭竣,如下

public class NetClassLoader extends ClassLoader {

    private String url;//class文件的URL

    public NetClassLoader(String url) {
        this.url = url;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);
        if(data == null){
            throw new ClassNotFoundException();
        }
        return this.defineClass(className,data,0,data.length);
    }

    private byte[] loadClassData(String className){
        String path = classNameToPath(className);
        InputStream ins = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        try {
            URL url = new URL(path);
            ins = url.openStream();
            baos = new ByteArrayOutputStream();
            int bufferSize = 8192;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 讀取類文件的字節(jié)
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            //這里省略解密的過程.......
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(ins != null){
                try {
                    ins.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(baos != null){
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }

    private String classNameToPath(String className) {
        // 得到類文件的URL
        return url + "/" + className.replace('.', '/') + ".class";
    }
}

主要是在獲取字節(jié)碼流時的區(qū)別塘辅,從網(wǎng)絡(luò)直接獲取到字節(jié)流再轉(zhuǎn)車字節(jié)數(shù)組然后利用defineClass方法創(chuàng)建class對象,如果繼承URLClassLoader類則和前面文件路徑的實現(xiàn)是類似的皆撩,無需擔(dān)心路徑是filePath還是Url扣墩,因為URLClassLoader內(nèi)的URLClassPath對象會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認(rèn)類Loader去讀取對于的路徑或者url下的class文件扛吞。

熱部署類加載器

所謂的熱部署就是利用同一個class文件不同的類加載器在內(nèi)存創(chuàng)建出兩個不同的class對象(關(guān)于這點的原因前面已分析過呻惕,即利用不同的類加載實例),由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調(diào)用findLoadedClass()方法)滥比,如果被加載過亚脆,則直接從緩存獲取,不會重新加載盲泛。注意同一個類加載器的實例和同一個class文件只能被加載器一次濒持,多次加載將報錯,因此我們實現(xiàn)的熱部署必須讓同一個class文件可以根據(jù)不同的類加載器重復(fù)加載寺滚,以實現(xiàn)所謂的熱部署柑营。實際上前面的實現(xiàn)的MyClassLoader和MyURLClassLoader已具備這個功能,但前提是直接調(diào)用findClass()方法村视,而不是調(diào)用loadClass()方法官套,因為ClassLoader中l(wèi)oadClass()方法體中調(diào)用findLoadedClass()方法進(jìn)行了檢測是否已被加載,因此我們直接調(diào)用findClass()方法就可以繞過這個問題蚁孔,當(dāng)然也可以重新loadClass方法奶赔,但強烈不建議這么干。

雙親委派模型的破壞者-線程上下文類加載器

當(dāng)前類加載器(current ClassLoader)

  • 每個類都會使用自己的類加載器(即加載自身的類加載器)來加載其他類(值的是所依賴的類)
  • 如果ClassX引用了ClassY杠氢,那么ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)

線程上下文類加載器(Context ClassLoader)

  • 線程上下文類加載器(Context ClassLoader)是從JDK1.2開始引入的站刑,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader cl)分別用來獲取和設(shè)置線程上下文類加載器
  • 如果沒有通過setContextClassLoader(ClassLoader cl)進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器
  • Java應(yīng)用運行時的初始線程的上下文類加載器是系統(tǒng)類加載器修然,在線程中運行的代碼可以通過該類加載器來加載類與資源

線程上下文類加載器的重要性:

  • 在Java應(yīng)用中存在著很多服務(wù)提供者接口(Service Provider Interface笛钝,SPI),這些接口允許第三方為它們提供實現(xiàn)愕宋,如常見的 SPI 有 JDBC玻靡、JNDI等,這些 SPI 的接口屬于 Java 核心庫中贝,一般存在rt.jar包中囤捻,由Bootstrap類加載器加載,而 SPI 的第三方實現(xiàn)代碼則是作為Java應(yīng)用所依賴的 jar 包被存放在classpath路徑下邻寿,由于SPI接口中的代碼經(jīng)常需要加載具體的第三方實現(xiàn)類并調(diào)用其相關(guān)方法蝎土,但SPI的核心接口類是由引導(dǎo)類加載器來加載的视哑,而Bootstrap類加載器無法直接加載SPI的實現(xiàn)類
  • 父ClassLoader可以使用當(dāng)前線程Thread.currentThread().getContextClassLoader所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader加載的類的情況誊涯,即改變了雙親委托模型
  • 線程上下文類加載器就是當(dāng)前線程的Current ClassLoader挡毅,在雙親委托模型下,類加載是由下至上的暴构,即下層的類加載器會委托上層進(jìn)行加載跪呈。但是對于SPI來說,有些接口是Java核心庫所提供的取逾,而Java核心庫是由啟動類加載器所加載的耗绿,而這些接口的實現(xiàn)確來自于不同的jar包(廠商提供),Java的啟動類加載器是不會加載其他來源的jar包砾隅,這樣傳統(tǒng)的雙親委托模型就無法滿足SPI的要求误阻,而通過給當(dāng)前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實現(xiàn)對于接口的實現(xiàn)類的加載晴埂。

如下圖所示究反,以jdbc.jar加載為例:


從圖可知rt.jar核心包是由Bootstrap類加載器加載的,其內(nèi)包含SPI核心接口類儒洛,由于SPI中的類經(jīng)常需要調(diào)用外部實現(xiàn)類的方法奴紧,而jdbc.jar包含外部實現(xiàn)類(jdbc.jar存在于classpath路徑)無法通過Bootstrap類加載器加載,因此只能委派線程上下文類加載器把jdbc.jar中的實現(xiàn)類加載到內(nèi)存以便SPI相關(guān)類使用晶丘。顯然這種線程上下文類加載器的加載方式破壞了“雙親委派模型”,它在執(zhí)行過程中拋棄雙親委派加載鏈模式唐含,使程序可以逆向使用類加載器浅浮,當(dāng)然這也使得Java類加載器變得更加靈活。為了進(jìn)一步證實這種場景捷枯,不妨看看DriverManager類的源碼滚秩,DriverManager是Java核心rt.jar包中的類,該類用來管理不同數(shù)據(jù)庫的實現(xiàn)驅(qū)動即Driver淮捆,它們都實現(xiàn)了Java核心包中的java.sql.Driver接口郁油,如mysql驅(qū)動包中的com.mysql.jdbc.Driver,這里主要看看如何加載外部實現(xiàn)類攀痊,在DriverManager初始化時會執(zhí)行如下代碼

//DriverManager是Java核心包rt.jar的類
public class DriverManager {

    //省略不必要的代碼......

    static {
        loadInitialDrivers();//執(zhí)行該方法
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        //省略不必要的代碼......

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //加載外部的Driver的實現(xiàn)類
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        //省略不必要的代碼......
    }
}

在DriverManager類初始化時執(zhí)行了loadInitialDrivers()方法,在該方法中通過ServiceLoader.load(Driver.class);去加載外部實現(xiàn)的驅(qū)動類桐腌,ServiceLoader類會去讀取mysql的jdbc.jar下META-INF文件的內(nèi)容,如下所示:


而com.mysql.jdbc.Driver繼承類如下:

/**
 * Backwards compatibility to support apps that call <code>Class.forName("com.mysql.jdbc.Driver");</code>.
 */
public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

從注釋可以看出平常我們使用com.mysql.jdbc.Driver已被丟棄了苟径,取而代之的是com.mysql.cj.jdbc.Driver案站,也就是說官方不再建議我們使用如下代碼注冊mysql驅(qū)動

//不建議使用該方式注冊驅(qū)動類
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

而是直接去掉注冊步驟,如下即可:

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");

這樣ServiceLoader會幫助我們處理一切棘街,并最終通過load()方法加載蟆盐,看看load()方法實現(xiàn)

public static <S> ServiceLoader<S> load(Class<S> service) {
    //通過線程上下文類加載器加載
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

很明顯了確實通過線程上下文類加載器加載的承边,實際上核心包的SPI類對外部實現(xiàn)類的加載都是基于線程上下文類加載器執(zhí)行的,通過這種方式實現(xiàn)了Java核心代碼內(nèi)部去調(diào)用外部實現(xiàn)類石挂。我們知道線程上下文類加載器默認(rèn)情況下就是AppClassLoader博助,那為什么不直接通過getSystemClassLoader()獲取類加載器來加載classpath路徑下的類的呢?其實是可行的痹愚,但這種直接使用getSystemClassLoader()方法獲取AppClassLoader加載類有一個缺點富岳,那就是代碼部署到不同服務(wù)時會出現(xiàn)問題,如把代碼部署到Java Web應(yīng)用服務(wù)或者EJB之類的服務(wù)將會出問題里伯,因為這些服務(wù)使用的線程上下文類加載器并非AppClassLoader城瞎,而是Java Web應(yīng)用服自家的類加載器,類加載器不同疾瓮。脖镀,所以我們應(yīng)用該少用getSystemClassLoader()±堑纾總之不同的服務(wù)使用的可能默認(rèn)ClassLoader是不同的脂矫,但使用線程上下文類加載器總能獲取到與當(dāng)前程序執(zhí)行相同的ClassLoader,從而避免不必要的問題缀匕。ok~.關(guān)于線程上下文類加載器暫且聊到這泪姨,前面闡述的DriverManager類,大家可以自行看看源碼削祈,相信會有更多的體會翅溺,另外關(guān)于ServiceLoader本篇并沒有過多的闡述,畢竟我們主題是類加載器髓抑,但ServiceLoader是個很不錯的解耦機制咙崎,大家可以自行查閱其相關(guān)用法。

線程上下文類加載器的一般使用模式(獲取 - 使用 - 還原)

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
    Thread.currentThread().setContextClassLoader(targetTClassLoader);
    myMethod();
}finally{
    Thread.currentThread().setContextClassLoader(classLoader);
}
  • myMethod()里面調(diào)用了Thread.currentThread().getContextClassLoader()獲取當(dāng)前線程的上下文類加載器做某些事情
  • 如果一個類由類加載器A加載吨拍,那么這個類的依賴類也是由相同的類加載器所加載的(前提是該類沒有被加載)
  • ContextClassLoader的作用就是為了破壞Java類加載的雙親委托機制
  • 當(dāng)高層提供了統(tǒng)一的接口讓低層去實現(xiàn)褪猛,同時又要在高層加載(或?qū)嵗?低層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類

線程上下文類加載器的適用場景:

  • 當(dāng)高層提供了統(tǒng)一接口讓低層去實現(xiàn)羹饰,同時又要是在高層加載(或?qū)嵗┑蛯拥念悤r伊滋,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。

  • 當(dāng)使用本類托管類加載队秩,然而加載本類的ClassLoader未知時笑旺,為了隔離不同的調(diào)用者,可以取調(diào)用者各自的線程上下文類加載器代為托管馍资。

參考:
https://www.cnblogs.com/mybatis/p/9396135.html

https://blog.csdn.net/yangcheng33/article/details/52631940

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末燥撞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌物舒,老刑警劉巖色洞,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異冠胯,居然都是意外死亡火诸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門荠察,熙熙樓的掌柜王于貴愁眉苦臉地迎上來置蜀,“玉大人,你說我怎么就攤上這事悉盆《⒒纾” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵焕盟,是天一觀的道長秋秤。 經(jīng)常有香客問我,道長脚翘,這世上最難降的妖魔是什么灼卢? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮来农,結(jié)果婚禮上鞋真,老公的妹妹穿的比我還像新娘。我一直安慰自己沃于,他們只是感情好涩咖,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著繁莹,像睡著了一般抠藕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蒋困,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音敬辣,去河邊找鬼雪标。 笑死,一個胖子當(dāng)著我的面吹牛溉跃,可吹牛的內(nèi)容都是我干的村刨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼撰茎,長吁一口氣:“原來是場噩夢啊……” “哼嵌牺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤逆粹,失蹤者是張志新(化名)和其女友劉穎募疮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體僻弹,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡阿浓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹋绽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芭毙。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卸耘,靈堂內(nèi)的尸體忽然破棺而出退敦,到底是詐尸還是另有隱情,我是刑警寧澤蚣抗,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布侈百,位于F島的核電站,受9級特大地震影響忠聚,放射性物質(zhì)發(fā)生泄漏设哗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一两蟀、第九天 我趴在偏房一處隱蔽的房頂上張望网梢。 院中可真熱鬧,春花似錦赂毯、人聲如沸战虏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烦感。三九已至,卻和暖如春膛堤,著一層夾襖步出監(jiān)牢的瞬間手趣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工肥荔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绿渣,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓燕耿,卻偏偏與公主長得像中符,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子誉帅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353