能不能自己寫一個類叫java.lang.System/String浊吏?網(wǎng)上答案都是錯的--ClassLoader詳解

ClassLoader 是Java中的類加載器,其作用就是將class字節(jié)碼文件加載到j(luò)ava虛擬機內(nèi)存中舶担。本文詳細介紹了ClassLoader的實現(xiàn)機制。介紹完了ClassLoader椒涯,題目所提到的問題也就解決了柄沮。

類啟動過程

相信學(xué)習(xí)過java的都知道,我們平時寫的java代碼(*.java)是不能直接運行的废岂,只有編譯后生成的.class文件才能被jvm識別祖搓。那么具體過程是什么呢?
類從加載到虛擬機內(nèi)存中開始到卸載出內(nèi)存為止湖苞,生命周期包括: 加載拯欧、 驗證準備财骨、 解析镐作、 初始化使用隆箩、 卸載该贾。其中驗證、準備捌臊、解析杨蛋,統(tǒng)稱為鏈接。

類啟動過程

如上圖所示,加載逞力、驗證曙寡、準備、初始化和卸載這五個階段的順序是確定的寇荧,類的加載過程必須按照這個順序來按部就班地開始举庶,而解析階段則不一定,它在某些情況下可以在初始化階段后再開始揩抡。因為java支持運行時綁定户侥。

加載(Loading)

類的加載,指的是將類的.class文件中的二進制數(shù)據(jù)讀入到內(nèi)存中捅膘,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi)添祸,然后在堆區(qū)創(chuàng)建一個java.lang.Class對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)寻仗。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對象刃泌,Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口署尤。

加載.class文件的方式有:

  1. 從本地系統(tǒng)中直接加載
  2. 通過網(wǎng)絡(luò)下載.class文件
  3. 從zip耙替,jar等歸檔文件中加載.class文件
  4. 從專有數(shù)據(jù)庫中提取.class文件
  5. 將Java源文件動態(tài)編譯為.class文件

在了解了什么是類的加載后,回頭來再看jvm進行類加載階段都做了什么曹体。虛擬機需要完成以下三件事情:

  1. 通過一個類的全限定名稱來獲取定義此類的二進制字節(jié)流俗扇。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
  3. 在java堆中生成一個代表這個類的java.lang.Class對象箕别,作為方法區(qū)這些數(shù)據(jù)的訪問入口铜幽。

相對于類加載過程的其他階段,加載階段是開發(fā)期相對來說可控性比較強串稀,本文也是主要介紹這一階段除抛。

驗證(Verification)

驗證的目的,是為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機的要求母截,而且不會危害虛擬機自身的安全到忽。不同的虛擬機對類驗證的實現(xiàn)可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證清寇、元數(shù)據(jù)的驗證喘漏、字節(jié)碼驗證符號引用驗證

準備(Preparation)

類變量(即static修飾的字段變量)分配內(nèi)存华烟,并且設(shè)置該類變量的初始值即0(如static int i=5;這里只將i初始化為0翩迈,至于5的值將在初始化時賦值),這里不包含用final修飾的static盔夜,因為final在編譯的時候就會分配了负饲,注意這里不會為實例變量分配初始化搅方,類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到Java堆中。

兩個關(guān)鍵點吧慢,即內(nèi)存分配的對象以及初始化的類型赏表。

  • 內(nèi)存分配的對象检诗。Java 中的變量有類變量和實例變量兩種類型逢慌,類變量指的是被 static 修飾的變量,而其他所有類型的變量都屬于實例變量间狂。在準備階段,JVM只會為類變量分配內(nèi)存忙菠,而不會為實例變量分配內(nèi)存牛欢。實例變量的內(nèi)存分配需要等到初始化階段才開始傍睹。
  • 初始化的類型拾稳。在準備階段熊赖,JVM 會為類變量分配內(nèi)存震鹉,并為其初始化传趾。但是這里的初始化指的是為變量賦予 Java 語言中該數(shù)據(jù)類型的零值(如0浆兰、0L榕订、null劫恒、false等)两嘴,而不是用戶代碼里初始化的值憔辫。

解析(Resolution)

解析階段JVM 針對類或接口贰您、字段枉圃、類方法孽亲、接口方法、方法類型篮绿、方法句柄和調(diào)用點限定符 7 類引用進行解析亲配。這個階段的主要任務(wù)是將其在常量池中的符號引用替換成在內(nèi)存中的直接引用吼虎。

  • 符號引用(Symbolic Reference):符號引用思灰,以一組符號來描述所引用的目標(biāo)洒疚,符號引用可以是任何形式的字面量巍扛,符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)电湘,引用的目標(biāo)并不一定已經(jīng)在內(nèi)存中。

  • 直接引用(Direct Reference):直接引用瘾晃,可以是直接指向目標(biāo)的指針蹦误、相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的偶洋,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同玄窝,如果有了直接引用恩脂,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在俩块。

初始化(Initialization)

初始化,為類的靜態(tài)變量賦予正確的初始值壮啊,JVM負責(zé)對類進行初始化歹啼。在Java中對類變量進行初始值設(shè)定有兩種方式:

  • 定義靜態(tài)變量時指定初始值狸眼。如 private static String x="123";
  • 在靜態(tài)代碼塊里為靜態(tài)變量賦值岁钓。如 static{ x="123"; }

JVM初始化步驟:

  1. 假如這個類還沒有被加載和連接屡限,則程序先加載并連接該類
  2. 假如該類的直接父類還沒有被初始化钧大,則先初始化其直接父類
  3. 假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句

卸載(UnLoading)

在以下情況的時候瓜饥,Java虛擬機會結(jié)束生命周期:

  • 執(zhí)行了System.exit()方法
  • 程序正常執(zhí)行結(jié)束
  • 程序在執(zhí)行過程中遇到了異撑彝粒或錯誤而異常終止
  • 由于操作系統(tǒng)出現(xiàn)錯誤而導(dǎo)致Java虛擬機進程終止

以上只是粗略介紹了一下類的加載過程,本文不對此做詳細展開拦键。

初步介紹類加載器

前文已介紹了類的啟動過程芬为,其中第一步就是加載媚朦,“類加載器”的任務(wù)是,根據(jù)一個類的全限定名來讀取此類的二進制字節(jié)流到JVM中份氧,然后轉(zhuǎn)換為一個與目標(biāo)類對應(yīng)的java.lang.Class對象實例蜗帜。
在Java中蔬顾,一個類用其全限定類名(包括包名和類名)作為標(biāo)識诀豁;但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標(biāo)識。
虛擬機提供了3種類加載器纱新,啟動(Bootstrap)類加載器脸爱、擴展(Extension)類加載器應(yīng)用程序(Application)類加載器(也稱系統(tǒng)類加載器)族檬。

啟動(Bootstrap)類加載器

Bootstrap類加載器主要加載的是JVM自身需要的類,這個加載器是使用C++語言實現(xiàn)的扫尖,是虛擬機自身的一部分换怖,它負責(zé)將 <JAVA_HOME>/lib 路徑下的核心類庫条摸,或 -Xbootclasspath 參數(shù)指定的路徑下的jar包加載到內(nèi)存中屈溉,注意由于虛擬機是按照文件名識別加載jar包的,如rt.jar线梗,如果文件名不被虛擬機識別仪搔,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為java煮嫌、javax、sun等開頭的類)懦冰。

擴展(Extension)類加載器

擴展類加載器是指 sun.misc.Launcher$ExtClassLoader類刷钢,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類瓤鼻,它負責(zé)加載 <JAVA_HOME>/lib/ext 目錄下,或者系統(tǒng)變量 -Djava.ext.dir 指定路徑中的類庫祭犯,開發(fā)者可以直接使用標(biāo)準擴展類加載器粥惧。

// ExtClassLoader 部分代碼
static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;
        ...
//加載<JAVA_HOME>/lib/ext目錄中的類庫
        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }
        ...
    }

應(yīng)用程序(Application)類加載器

也叫系統(tǒng)類加載器突雪,應(yīng)用程序加載器 sun.misc.Launcher$AppClassLoader 類。它負責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類庫督函,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用應(yīng)用程序類加載器宛篇,一般情況下該類加載是程序中默認的類加載器豌鸡,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器涯冠。

//AppClassLoader 部分代碼
    static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的砸逊,在必要時师逸,我們還可以自定義類加載器篓像。
需要注意的是盒粮,Java虛擬機對class文件采用的是按需加載的方式丹皱,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象,而且加載某個類的class文件時爽室,Java虛擬機采用的是雙親委派模型(Parent Delegation Model)

雙親委派模型

該模型要求除了頂層的啟動類加載器外啸箫,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。子類加載器和父類加載器不是以繼承(Inheritance)的關(guān)系來實現(xiàn)扎唾,而是通過組合(Composition)關(guān)系來復(fù)用父加載器的代碼。
雙親委派模型的工作過程為:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類逗威,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此尽纽,因此所有的類加載請求都會傳給頂層的啟動類加載器春锋,只有當(dāng)父加載器反饋自己無法完成該加載請求(該加載器的搜索范圍中沒有找到對應(yīng)的類)時,子加載器才會嘗試自己去加載呐萌。

在這里插入圖片描述

使用這種模型來組織類加載器之間的關(guān)系的好處,是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。通過這種層級關(guān)可以避免類的重復(fù)加載茫叭,當(dāng)父親加載器已經(jīng)加載了該類時,就子加載器就不會再加載一次莽囤。例如java.lang.Object類怯屉,無論哪個類加載器去加載該類赌躺,最終都是由啟動類加載器進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類缅叠。否則的話弹囚,如果不使用該模型的話领曼,如果用戶自定義一個java.lang.Object類且存放在classpath中鸥鹉,那么系統(tǒng)中將會出現(xiàn)多個Object類,應(yīng)用程序也會變得很混亂庶骄。如果我們自定義一個rt.jar中已有類的同名Java類毁渗,會發(fā)現(xiàn)JVM可以正常編譯,但該類永遠無法被加載運行灸异。

詳解類加載器

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


在這里插入圖片描述

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

  • loadClass(String)
    該方法加載指定名稱(包括包名)的二進制類型儡嘶,該方法在JDK1.2之后不再建議用戶重寫,但用戶可以直接調(diào)用該方法恍风,loadClass()方法是ClassLoader類自己實現(xiàn)的蹦狂,該方法中的邏輯就是雙親委派模式的實現(xiàn),其源碼如下朋贬,loadClass(String name, boolean resolve)是一個重載方法凯楔,resolve參數(shù)代表是否生成class對象的同時進行解析相關(guān)操作。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 先從緩存查找該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.
                    // 如果都沒有找到糠亩,則通過自定義實現(xiàn)的findClass去查找并加載
                    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;
        }
    }

正如loadClass方法所展示的虐骑,當(dāng)類加載請求到來時,先從緩存中查找該類對象赎线,如果存在直接返回廷没,如果不存在則交給該類加載器的父加載器去加載,倘若沒有父加載則交給頂級啟動類加載器去加載垂寥,最后倘若仍沒有找到颠黎,則使用findClass()方法去加載另锋。

  • findClass(String)
    在JDK1.2之前,在JDK1.2之后已不再建議用戶去覆蓋loadClass() 方法狭归,而是建議把自定義的類加載邏輯寫在findClass() 方法中夭坪,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的过椎,當(dāng)loadClass()方法中父加載器加載失敗后台舱,則會調(diào)用自己的 findClass() 方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模型潭流。ClassLoader類中findClass()方法源碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
  • defineClass(String name, 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對象,URLClassLOader中findClass代碼如下:
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                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;
    }
  • resolveClass(Class??? c)
    該方法可以使類的Class對象創(chuàng)建完成同時被解析炎滞。前面我們說鏈接階段主要是對字節(jié)碼進行驗證敢艰,為類變量分配內(nèi)存并設(shè)置初始值同時將字節(jié)碼文件中的符號引用轉(zhuǎn)換為直接引用。

上述4個方法是ClassLoader類中的比較重要且常用的方法册赛。SercureClassLoader 擴展了 ClassLoader钠导,新增了幾個與使用相關(guān)的代碼源(對代碼源的位置及其證書的驗證)和權(quán)限定義類驗證(主要指對class源碼的訪問權(quán)限)的方法,一般我們不會直接跟這個類打交道森瘪,更多是與它的子類 URLClassLoader 有所關(guān)聯(lián)抖格,前面說過洽沟,ClassLoader是一個抽象類抛计,很多方法是空的沒有實現(xiàn)蹋辅,比如 findClass()、findResource()等窗宇。而URLClassLoader這個實現(xiàn)類為這些方法提供了具體的實現(xiàn)措伐。

在這里插入圖片描述

這里額外介紹一下 sun.misc.URLClassPath 類,通過這個類就可以找到要加載的字節(jié)碼流担映,也就是說URLClassPath類負責(zé)找到要加載的字節(jié)碼废士,再讀取成字節(jié)流叫潦,如 URLClassLOader中findClass代碼中有下面這句:

Resource res = ucp.getResource(path, false);

這里的 ucp 就是 URLClassPath蝇完,。如上類圖所示,URLClassPath 有3個內(nèi)部類短蜕,分別是FileLoader氢架、JarLoader、Loader朋魔,加載的字節(jié)碼流的具體工作就是由這些內(nèi)部類完成岖研。至于如何分配,在創(chuàng)建URLClassPath對象時警检,會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包孙援,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認Loader類。


在這里插入圖片描述

了解完URLClassLoader后接著看看剩余的兩個類加載器扇雕,即拓展類加載器ExtClassLoader和應(yī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()方法玻侥,因此依然遵守雙親委派模式。

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

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

  • 啟動類加載器使碾,由C++實現(xiàn),沒有父類祝懂。
  • 拓展類加載器(ExtClassLoader)票摇,由Java語言實現(xiàn),父類加載器為null
  • 系統(tǒng)類加載器(AppClassLoader)砚蓬,由Java語言實現(xiàn)矢门,父類加載器為ExtClassLoader
  • 自定義類加載器,在沒有指定的情況下灰蛙,父類加載器默認為當(dāng)前系統(tǒng)加載器祟剔,即 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并把var1作為父加載器傳遞給AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        ...
    }

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

public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

再來看下自定義類加載器组力,ClassLoader 有兩個protected類型的構(gòu)造器,如下:

protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

因此我們自定義類加載器時抖拴,可以指定父類加載器燎字,若未指定則默認為系統(tǒng)類加載器:

public class MyClassLoader extends ClassLoader {
    //類存放的路徑
    private String rootDir;

    public MyClassLoader(String rootDir) {
        super(ClassLoader.getSystemClassLoader().getParent());
        this.rootDir = rootDir;
    }
    。阿宅。候衍。

編寫自己的類加載器

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

  • 當(dāng)class文件不在ClassPath路徑下榨为,默認系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實現(xiàn)一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象煌茴。

  • 當(dāng)一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進行相應(yīng)的加密操作時随闺,需要先對class文件進行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實現(xiàn)相應(yīng)的邏輯蔓腐。

  • 當(dāng)需要實現(xiàn)熱部署功能時(一個class文件通過不同的類加載器產(chǎn)生不同class對象從而實現(xiàn)熱部署功能)矩乐,需要實現(xiàn)自定義ClassLoader的邏輯。

好了回论,下面開始編寫自己的類加載器散罕,代碼結(jié)構(gòu)如下:


在這里插入圖片描述

MyClassLoader 代碼如下:

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader {
    //類存放的路徑
    private String rootDir;

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

    /**
     * 重寫findClass方法
     */
    @Override
    public Class<?> findClass(String name) {
        byte[] data = loadClassData(name);
        // 調(diào)用父類的 defineClass 方法
        return this.defineClass(name, data, 0, data.length);
    }

    /**
     * 編寫獲取class文件并轉(zhuǎn)換為字節(jié)碼流的邏輯
     * @param name
     * @return
     */
    public byte[] loadClassData(String name) {
        try {
            name = rootDir + name.replace(".", File.separator) + ".class";
            FileInputStream is = new FileInputStream(name);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = is.read()) != -1) {
                baos.write(b);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

MyURLClassLoader 代碼如下:

package classloader;

import java.net.URL;
import java.net.URLClassLoader;

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

可以看到的是,繼承自 URLClassLoader 的類加載器代碼要比繼承自 ClassLoader 的類繼承器代碼簡單很多傀蓉,這也符合我們上文的介紹欧漱。
再也看下如何使用自定義的類加載器。
Animal代碼如下:

package classloader;

class Animal {
    public void say() {
        System.out.println("hello world!");
    }
}

ClassLoaderTest 代碼:

package classloader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;

public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException {
        String rootDir = "/Users/lvbing/classloader/";
        String className = "classloader.Animal";

        myClassLoaderTest(rootDir, className);
        myURLClassLoaderTest(rootDir, className);
    }

    private static void myClassLoaderTest(String rootDir, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //新建一個類加載器
        MyClassLoader cl = new MyClassLoader(rootDir);
        //加載類葬燎,得到Class對象
        Class<?> clazz = cl.loadClass(className);
        //得到類的實例
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println("loadClass->hashCode:" + clazz.hashCode());
    }

    private static void myURLClassLoaderTest(String rootDir, String className) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        //創(chuàng)建自定義文件類加載器
        File file = new File(rootDir);
        //File to URI
        URI uri = file.toURI();
        URL[] urls = {uri.toURL()};

        MyURLClassLoader loader = new MyURLClassLoader(urls);

        //加載指定的class文件
        Class<?> clazz = loader.loadClass(className);
        //得到類的實例
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println("loadClass->hashCode:" + clazz.hashCode());
    }

}

我們運行下 ClassLoaderTest误甚,看看結(jié)果:

sun.misc.Launcher$AppClassLoader@18b4aac2
loadClass->hashCode:1625635731
sun.misc.Launcher$AppClassLoader@18b4aac2
loadClass->hashCode:1625635731

可以看到真正加載Animal類的加載器是AppClassLoader,這是因為Animal類已經(jīng)在classPath路徑下谱净,這也驗證了雙親委派模型窑邦。至于為什么hashCode 也一致,可以看下上述關(guān)于loadClass方法的介紹壕探,這是先從緩存查找的結(jié)果冈钦。需要注意的是,這個緩存是與ClassLoader的實例綁定的李请,不同的ClassLoader實例緩存也不一樣瞧筛。
我們現(xiàn)在把Animal.class移到測試目錄厉熟,并把Animal類注釋掉,再運行一次:

classloader.MyClassLoader@60e53b93
loadClass->hashCode:491044090
classloader.MyURLClassLoader@6f94fa3e
loadClass->hashCode:1725154839

結(jié)果完全符合預(yù)期驾窟。
我們修改一下測試代碼庆猫,調(diào)用兩次myClassLoaderTest方法:

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException {
        String rootDir = "/Users/lvbing/classloader/";
        String className = "classloader.Animal";

        myClassLoaderTest(rootDir, className);
        myClassLoaderTest(rootDir, className);
//        myURLClassLoaderTest(rootDir, className);
    }

結(jié)果如下:

classloader.MyClassLoader@60e53b93
loadClass->hashCode:491044090
classloader.MyClassLoader@266474c2
loadClass->hashCode:1581781576

可以看到hashCode也不一致认轨,這就是因為每個ClassLoader實例對象都有自己的緩存绅络。
這里可以延伸一個概念,在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件

  • 類的完整類名必須一致嘁字,包括包名恩急。
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。

也就是說纪蜒,在JVM中衷恭,即使這個兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載纯续,但只要加載它們的ClassLoader實例對象不同随珠,那么這兩個類對象也是不相等的。

能不能自己寫一個類叫java.lang.System/String

這是網(wǎng)上的一道面試題猬错,先說答案:窗看。
下面通過代碼來驗證。
新增Math類

package java.lang;

public class Math {
    public void say(){
        System.out.println("hello Math!");
    }
}

注意包名倦炒,編譯后移走Math.class到測試目錄:


image.png

在ClassLoaderTest類中新增代碼:

private static void mathTest(String rootDir) throws IllegalAccessException, InstantiationException {
        String className = "java.lang.Math";
        //新建一個類加載器
        MyClassLoader cl = new MyClassLoader(rootDir);
        //注意這里用的findClass方法显沈,為了避開緩存
        Class<?> clazz = cl.findClass(className);
        //得到類的實例
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println("loadClass->hashCode:" + clazz.hashCode());
    }

運行結(jié)果如下:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
    at classloader.MyClassLoader.findClass(MyClassLoader.java:22)
    at classloader.ClassLoaderTest.mathTest(ClassLoaderTest.java:51)
    at classloader.ClassLoaderTest.main(ClassLoaderTest.java:15)

ClassLoader.java的655行拋出了一個SecurityException異常,看下這段代碼:

private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) checkCerts(name, pd.getCodeSource());

        return pd;
    }

很明顯逢唤,在name不為null的情況下拉讯,會檢查name是否以"java."開頭,那如果name為null呢鳖藕?


在這里插入圖片描述

再次運行:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
    at classloader.MyClassLoader.findClass(MyClassLoader.java:22)
    at classloader.ClassLoaderTest.mathTest(ClassLoaderTest.java:51)
    at classloader.ClassLoaderTest.main(ClassLoaderTest.java:15)

依然拋出異常魔慷,不過這次不再是preDefineClass拋出的了,而是一個native方法:

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);

defineClass底層調(diào)用的是native方法著恩,并且defineClass是protected final的盖彭,無法重寫(能重寫也沒用)。
等等页滚,上面的答案不是說能嗎召边?怎么到現(xiàn)在為止都不行呢?
還記得開頭關(guān)于Bootstrap ClassLoader的介紹嗎裹驰?我們可以通過 -Xbootclasspath 參數(shù)來指定 Bootstrap加載目錄隧熙。具體介紹如下:

參數(shù) 效果
-Xbootclasspath:<path> 完全取代核心的Java class 搜索路徑。不常用幻林,否則要重新寫所有Java 核心class
-Xbootclasspath/p:<path> 前綴在核心class搜索路徑前面贞盯。也就是優(yōu)先搜索參數(shù)指定路徑音念。不常用,避免引起不必要的沖突.
-Xbootclasspath/a:<path> 后綴在核心class搜索路徑后面躏敢。也就是其他路徑都搜完了闷愤,再搜索參數(shù)指定路徑件余。常用

這里我們使用-Xbootclasspath/p:<path>參數(shù)讥脐。先把系統(tǒng)的Math類代碼復(fù)制到我們自己寫的Math類中,再把當(dāng)前項目打成jar包啼器,最后執(zhí)行如下命令:

 java -Xbootclasspath/a:/Users/lvbing/classloader/java-advanced-1.0-SNAPSHOT.jar -verbose > test.txt

打開test.txt文件旬渠,發(fā)現(xiàn)如下記錄:


在這里插入圖片描述

在這里插入圖片描述

可以看到j(luò)ava.lang.Math加載自我們自己打包的jar中,由此可見端壳,我們確實可以自己編寫以"java."開頭的代碼告丢,但必須交由Bootstrap ClassLoader加載。

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

在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的核心接口類是由啟動類加載器來加載的赢赊,而Bootstrap類加載器無法直接加載SPI的實現(xiàn)類,同時由于雙親委派模式的存在级历,Bootstrap類加載器也無法反向委托AppClassLoader加載器SPI的實現(xiàn)類释移。在這種情況下,我們就需要一種特殊的類加載器來加載第三方的類庫寥殖,而線程上下文類加載器就是很好的選擇玩讳。

線程上下文類加載器(contextClassLoader)是從 JDK 1.2 開始引入的,我們可以通過java.lang.Thread類中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法來獲取和設(shè)置線程的上下文類加載器嚼贡。如果沒有手動設(shè)置上下文類加載器熏纯,線程將繼承其父線程的上下文類加載器,初始線程的上下文類加載器是系統(tǒng)類加載器(AppClassLoader),在線程中運行的代碼可以通過此類加載器來加載類和資源粤策,如下圖所示樟澜,以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類加載器變得更加靈活想际。
為了進一步證實這種場景,不妨看看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í)行如下代碼傻昙。

public class DriverManager {
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    private static void loadInitialDrivers() {
        ...
        
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                ...
            }
        });
        ...
    }

在DriverManager類初始化時執(zhí)行了loadInitialDrivers()方法,在該方法中通過ServiceLoader.load(Driver.class);去加載外部實現(xiàn)的驅(qū)動類。
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)類。我們知道線程上下文類加載器默認情況下就是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ù)使用的可能默認ClassLoader是不同的窃款,但使用線程上下文類加載器總能獲取到與當(dāng)前程序執(zhí)行相同的ClassLoader课兄,從而避免不必要的問題。

以上測試代碼已上傳至github:https://github.com/lvbabc/practice-demo/tree/main/java-advanced

參考資料
https://blog.csdn.net/javazejian/article/details/73413292
https://blog.csdn.net/m0_43452671/article/details/89892706
https://juejin.im/post/6844903564804882445

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晨继,一起剝皮案震驚了整個濱河市烟阐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌踱稍,老刑警劉巖曲饱,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悠抹,死亡現(xiàn)場離奇詭異,居然都是意外死亡扩淀,警方通過查閱死者的電腦和手機楔敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驻谆,“玉大人卵凑,你說我怎么就攤上這事∈る” “怎么了勺卢?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長象对。 經(jīng)常有香客問我黑忱,道長,這世上最難降的妖魔是什么勒魔? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任甫煞,我火速辦了婚禮,結(jié)果婚禮上冠绢,老公的妹妹穿的比我還像新娘抚吠。我一直安慰自己,他們只是感情好弟胀,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布楷力。 她就那樣靜靜地躺著,像睡著了一般孵户。 火紅的嫁衣襯著肌膚如雪萧朝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天延届,我揣著相機與錄音剪勿,去河邊找鬼。 笑死方庭,一個胖子當(dāng)著我的面吹牛厕吉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播械念,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼头朱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了龄减?” 一聲冷哼從身側(cè)響起项钮,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后烁巫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體署隘,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年亚隙,在試婚紗的時候發(fā)現(xiàn)自己被綠了磁餐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡阿弃,死狀恐怖诊霹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渣淳,我是刑警寧澤脾还,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站入愧,受9級特大地震影響鄙漏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砂客,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一泥张、第九天 我趴在偏房一處隱蔽的房頂上張望呵恢。 院中可真熱鬧鞠值,春花似錦、人聲如沸渗钉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳄橘。三九已至声离,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘫怜,已是汗流浹背术徊。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鲸湃,地道東北人赠涮。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像暗挑,于是被迫代替她去往敵國和親笋除。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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