詳細深刻分析 Java ClassLoader 工做機制

申明:本文首發於 詳細深刻分析 ClassLoader 工做機制 ,若有轉載,註明原出處便可,謝謝配合。java


什麼是 ClassLoader ?

你們都知道,當咱們寫好一個 Java 程序以後,不是管是 C/S 仍是 B/S 應用,都是由若干個 .class 文件組織而成的一個完整的 Java 應用程序,當程序在運行時,即會調用該程序的一個入口函數來調用系統的相關功能,而這些功能都被封裝在不一樣的 class 文件當中,因此常常要從這個 class 文件中要調用另一個 class 文件中的方法,若是另一個文件不存在的,則會引起系統異常。而程序在啓動的時候,並不會一次性加載程序所要用的全部class文件,而是根據程序的須要,經過Java的類加載機制(ClassLoader)來動態加載某個 class 文件到內存當中的,從而只有 class 文件被載入到了內存以後,才能被其它 class 所引用。因此 ClassLoader 就是用來動態加載 class 文件到內存當中用的。web

ClassLoader 做用:

  • 負責將 Class 加載到 JVM 中算法

  • 審查每一個類由誰加載(父優先的等級加載機制)數據庫

  • 將 Class 字節碼從新解析成 JVM 統一要求的對象格式apache

一、ClassLoader 類結構分析

爲了更好的理解類的加載機制,咱們來深刻研究一下 ClassLoader 和他的方法。api

public abstract class ClassLoader數組

ClassLoader類是一個抽象類,sun公司是這麼解釋這個類的:安全

/**
 * A class loader is an object that is responsible for loading classes. The
 * class ClassLoader is an abstract class.  Given the binary name of a class, a class loader should attempt to
 * locate or generate data that constitutes a definition for the class.  A
 * typical strategy is to transform the name into a file name and then read a
 * "class file" of that name from a file system.
**/

大體意思以下:服務器

class loader 是一個負責加載 classes 的對象,ClassLoader 類是一個抽象類,須要給出類的二進制名稱,class loader 嘗試定位或者產生一個 class 的數據,一個典型的策略是把二進制名字轉換成文件名而後到文件系統中找到該文件。網絡

如下是 ClassLoader 經常使用到的幾個方法及其重載方法:

  • ClassLoader

  • defineClass(byte[], int, int) 把字節數組 b中的內容轉換成 Java 類,返回的結果是java.lang.Class類的實
    例。這個方法被聲明爲final的

  • findClass(String name) 查找名稱爲 name的類,返回的結果是java.lang.Class類的實例

  • loadClass(String name) 加載名稱爲 name的類,返回的結果是java.lang.Class類的實例

  • resolveClass(Class<?>) 連接指定的 Java 類

其中 defineClass 方法用來將 byte 字節流解析成 JVM 可以識別的 Class 對象,有了這個方法意味着咱們不只僅能夠經過 class 文件實例化對象,還能夠經過其餘方式實例化對象,若是咱們經過網絡接收到一個類的字節碼,拿到這個字節碼流直接建立類的 Class 對象形式實例化對象。若是直接調用這個方法生成類的 Class 對象,這個類的 Class 對象尚未 resolve ,這個 resolve 將會在這個對象真正實例化時才進行。

接下來咱們看loadClass方法的實現方式:

protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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();
                    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;
        }
    }

該方法大概意思:

使用指定的二進制名稱來加載類,這個方法的默認實現按照如下順序查找類: 調用findLoadedClass(String) 方法檢查這個類是否被加載過 使用父加載器調用 loadClass(String) 方法,若是父加載器爲 Null,類加載器裝載虛擬機內置的加載器調用 findClass(String) 方法裝載類, 若是,按照以上的步驟成功的找到對應的類,而且該方法接收的 resolve 參數的值爲 true,那麼就調用resolveClass(Class) 方法來處理類。 ClassLoader 的子類最好覆蓋 findClass(String) 而不是這個方法。 除非被重寫,這個方法默認在整個裝載過程當中都是同步的(線程安全的)。

二、ClassLoader 的等級加載機制

Java默認提供的三個ClassLoader

  • BootStrap ClassLoader:稱爲啓動類加載器,是Java類加載層次中最頂層的類加載器,負責加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等,可經過以下程序得到該類加載器從哪些地方加載了相關的jar或class文件:

    public class BootStrapTest
    {
        public static void main(String[] args)
        {
          URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
          for (int i = 0; i < urls.length; i++) {
              System.out.println(urls[i].toExternalForm());
           }
        }
    }

如下內容是上述程序從本機JDK環境所得到的結果:

這裏寫圖片描述

其實上述結果也是經過查找 sun.boot.class.path 這個系統屬性所得知的。

System.out.println(System.getProperty("sun.boot.class.path"));

這裏寫圖片描述

打印結果:C:\Java\jdk1.8.0_60\jre\lib\resources.jar;C:\Java\jdk1.8.0_60\jre\lib\rt.jar;C:\Java\jdk1.8.0_60\jre\lib\sunrsasign.jar;C:\Java\jdk1.8.0_60\jre\lib\jsse.jar;C:\Java\jdk1.8.0_60\jre\lib\jce.jar;C:\Java\jdk1.8.0_60\jre\lib\charsets.jar;C:\Java\jdk1.8.0_60\jre\lib\jfr.jar;C:\Java\jdk1.8.0_60\jre\classes
  • Extension ClassLoader:稱爲擴展類加載器,負責加載Java的擴展類庫,Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。默認加載JAVA_HOME/jre/lib/ext/目下的全部jar。

  • App ClassLoader:稱爲系統類加載器,負責加載應用程序classpath目錄下的全部jar和class文件。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。

除了系統提供的類加載器之外,開發人員能夠經過繼承java.lang.ClassLoader類的方式實現本身的類加載器,以知足一些特殊的需求。

除了引導類加載器以外,全部的類加載器都有一個父類加載器。 給出的 getParent()方法能夠獲得。對於
系統提供的類加載器來講,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來講,其父類加載器是加載此類加載器 Java 類的類加載器。由於類加載器 Java 類如同其它的 Java 類同樣,也是要由類加載器來加載的。通常來講,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器經過這種方式組織起來,造成樹狀結構。樹的根節點就是引導類加載器。

ClassLoader加載類的原理

1. 原理介紹

ClassLoader使用的是雙親委託模型來搜索類的,每一個ClassLoader實例都有一個父類加載器的引用(不是繼承的關係,是一個包含的關係),虛擬機內置的類加載器(Bootstrap ClassLoader)自己沒有父類加載器,但能夠用做其它ClassLoader實例的的父類加載器。當一個ClassLoader實例須要加載某個類時,它會試圖親自搜索某個類以前,先把這個任務委託給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,若是沒加載到,則把任務轉交給Extension ClassLoader試圖加載,若是也沒加載到,則轉交給App ClassLoader 進行加載,若是它也沒有加載獲得的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。若是它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。不然將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。

二、爲何要使用雙親委託這種模型呢?

由於這樣能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要 ClassLoader再加載一次。考慮到安全因素,咱們試想一下,若是不使用這種委託模式,那咱們就能夠隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在很是大的安全隱患,而雙親委託的方式,就能夠避免這種狀況,由於String已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,因此用戶自定義的ClassLoader永遠也沒法加載一個本身寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。

三、 可是JVM在搜索類的時候,又是如何斷定兩個class是相同的呢?

JVM在斷定兩個class是否相同時,不只要判斷兩個類名是否相同,並且要判斷是否由同一個類加載器實例加載的。只有二者同時知足的狀況下,JVM才認爲這兩個class是相同的。就算兩個class是同一份class字節碼,若是被兩個不一樣的ClassLoader實例所加載,JVM也會認爲它們是兩個不一樣class。好比網絡上的一個Java類org.classloader.simple.NetClassLoaderSimple,javac編譯以後生成字節碼文件NetClassLoaderSimple.classClassLoaderAClassLoaderB這兩個類加載器並讀取了NetClassLoaderSimple.class文件,並分別定義出了java.lang.Class實例來表示這個類,對於JVM來講,它們是兩個不一樣的實例對象,但它們確實是同一份字節碼文件,若是試圖將這個Class實例生成具體的對象進行轉換時,就會拋運行時異常java.lang.ClassCaseException,提示這是兩個不一樣的類型。如今經過實例來驗證上述所描述的是否正確:
1)、在web服務器上建一個org.classloader.simple.NetClassLoaderSimple.java

public class NetClassLoaderSimple
{
    private NetClassLoaderSimple instance;
    public void setNetClassLoaderSimple(Object object){
        this.instance = (NetClassLoaderSimple)object;
    }
}

org.classloader.simple.NetClassLoaderSimple類的setNetClassLoaderSimple方法接收一個Object類型參數,並將它強制轉換成org.classloader.simple.NetClassLoaderSimple類型。

2)、測試兩個class是否相同 NetWorkClassLoader.java

package classloader;

public class NewworkClassLoaderTest {

    public static void main(String[] args) {
        try {
            //測試加載網絡中的class文件
            String rootUrl = "http://localhost:8080/httpweb/classes";
            String className = "org.classloader.simple.NetClassLoaderSimple";
            NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
            NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
            Class<?> clazz1 = ncl1.loadClass(className);
            Class<?> clazz2 = ncl2.loadClass(className);
            Object obj1 = clazz1.newInstance();
            Object obj2 = clazz2.newInstance();
            clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

首先得到網絡上一個class文件的二進制名稱,而後經過自定義的類加載器NetworkClassLoader建立兩個實例,並根據網絡地址分別加載這份class,並獲得這兩個ClassLoader實例加載後生成的Class實例clazz1和clazz2,最後將這兩個Class實例分別生成具體的實例對象obj1和obj2,再經過反射調用clazz1中的setNetClassLoaderSimple方法。

3)、查看測試結果

結論:從結果中能夠看出,運行時拋出了java.lang.ClassCastException異常。雖然兩個對象obj1和 obj2的類的名字相同,可是這兩個類是由不一樣的類加載器實例來加載的,因此JVM認爲它們就是兩個不一樣的類。

瞭解了這一點以後,就能夠理解代理模式的設計動機了。代理模式是爲了保證 Java 核心庫的類型安全。全部 Java 應用都至少須要引用 java.lang.Object類,也就是說在運行的時候,java.lang.Object這個類須要被加載到 Java 虛擬機中。若是這個加載過程由 Java 應用本身的類加載器來完成的話,極可能就存在多個版本的 java.lang.Object類,並且這些類之間是不兼容的。經過代理模式,對於 Java 核心庫的類的加載工做由引導類加載器來統一完成,保證了Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。

不一樣的類加載器爲相同名稱的類建立了額外的名稱空間。相同名稱的類能夠並存在 Java 虛擬機中,只須要用不一樣的類加載器來加載它們便可。不一樣類加載器加載的類之間是不兼容的,這就至關於在 Java 虛擬機內部建立了一個個相互隔離的 Java 類空間。

ClassLoader的體系架構:

類加載器的樹狀組織結構

測試一

public class ClassLoaderTree
{
    public static void main(String[] args) {
        ClassLoader loader = ClassLoaderTree.class.getClassLoader();
        while (loader!=null){
            System.out.println(loader.toString());
            loader = loader.getParent();
        }
        System.out.println(loader);
    }
}

每一個 Java 類都維護着一個指向定義它的類加載器的引用,經過 getClassLoader()方法就能夠獲取到此引用。代碼中經過遞歸調用 getParent()方法來輸出所有的父類加載器。

結果是:

這裏寫圖片描述

第一個輸出的是 ClassLoaderTree類的類加載器,即系統類加載器。它是sun.misc.Launcher$AppClassLoader類的實例;第二個輸出的是擴展類加載器,是sun.misc.Launcher$ExtClassLoader類的實例。須要注意的是這裏並無輸出引導類加載器,這是因爲有些 JDK 的實現對於父類加載器是引導類加載器的狀況,getParent()方法返回 null。第三行結果說明:ExtClassLoader的類加器是Bootstrap ClassLoader,由於Bootstrap ClassLoader不是一個普通的Java類,因此ExtClassLoaderparent=null,因此第三行的打印結果爲null就是這個緣由。

測試二

將ClassLoaderTree.class打包成ClassLoaderTree.jar,放到Extension ClassLoader的加載目錄下(JAVA_HOME/jre/lib/ext),而後從新運行這個程序,獲得的結果會是什麼樣呢?

這裏寫圖片描述

這裏寫圖片描述

此處我在 IDEA 中的運行結果還和上面的同樣,與文章 深刻分析Java ClassLoader原理 中的有差距,具體緣由未弄清楚,還但願讀者可以親自測試。

那文章中的結果是:

打印結果分析:
爲何第一行的結果是ExtClassLoader呢?

由於 ClassLoader 的委託模型機制,當咱們要用 ClassLoaderTest.class 這個類的時候,AppClassLoader 在試圖加載以前,先委託給 Bootstrcp ClassLoader,Bootstracp ClassLoader 發現本身沒找到,它就告訴 ExtClassLoader,兄弟,我這裏沒有這個類,你去加載看看,而後 Extension ClassLoader 拿着這個類去它指定的類路徑(JAVA_HOME/jre/lib/ext)試圖加載,唉,它發如今ClassLoaderTest.jar 這樣一個文件中包含 ClassLoaderTest.class 這樣的一個文件,而後它把找到的這個類加載到內存當中,並生成這個類的 Class 實例對象,最後把這個實例返回。因此 ClassLoaderTest.class 的類加載器是 ExtClassLoader。

第二行的結果爲null,是由於ExtClassLoader的父類加載器是Bootstrap ClassLoader。

JVM加載class文件的兩種方法;

  • 隱式加載, 程序在運行過程當中當碰到經過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中。

  • 顯式加載, 經過class.forname()、this.getClass.getClassLoader().loadClass()等方法顯式加載須要的類,或者咱們本身實現的 ClassLoader 的 findlass() 方法。

下面介紹下 class.forName的加載類方法:

Class.forName是一個靜態方法,一樣能夠用來加載類。該方法有兩種形式:Class.forName(String name,boolean initialize, ClassLoader loader)和Class.forName(String className)。第一種形式的參數 name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。第二種形式則至關於設置了參數 initialize的值爲 true,loader的值爲當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如
Class.forName("org.apache.derby.jdbc.EmbeddedDriver")用來加載 Apache Derby 數據庫的驅動。

類加載的動態性體現:

一個應用程序老是由n多個類組成,Java程序啓動時,並非一次把全部的類所有加載後再運行,它老是先把保證程序運行的基礎類一次性加載到jvm中,其它類等到jvm用到的時候再加載,這樣的好處是節省了內存的開銷,由於java最先就是爲嵌入式系統而設計的,內存寶貴,這是一種能夠理解的機制,而用到時再加載這也是java動態性的一種體現。

三、如何加載 class 文件

這裏寫圖片描述

  • 第一階段找到 .class 文件並把這個文件包含的字節碼加載到內存中。

  • 第二階段中分三步,字節碼驗證;class 類數據結構分析及相應的內存分配;最後的符號表的連接。

  • 第三階段是類中靜態屬性和初始化賦值,以及靜態塊的執行等。

3.1 、加載字節碼到內存

。。

3.2 、驗證與分析

  • 字節碼驗證,類裝入器對於類的字節碼要作不少檢測,以確保格式正確,行爲正確。

  • 類裝備,準備表明每一個類中定義的字段、方法和實現接口所必須的數據結構。

  • 解析,裝入器裝入類所引用的其餘全部類。

四、常見加載類錯誤分析

4.1 、 ClassNotFoundExecption

ClassNotFoundExecption 異常是日常碰到的最多的。這個異常一般發生在顯示加載類的時候。

public class ClassNotFoundExceptionTest
{
    public static void main(String[] args) {
        try {
            Class.forName("NotFoundClass");
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

這裏寫圖片描述

顯示加載一個類一般有:

  • 經過類 Class 中的 forName() 方法

  • 經過類 ClassLoader 中的 loadClass() 方法

  • 經過類 ClassLoader 中的 findSystemClass() 方法

出現這種錯誤其實就是當 JVM 要加載指定文件的字節碼到內存時,並無找到這個文件對應的字節碼,也就是這個文件並不存在。解決方法就是檢查在當前的 classpath 目錄下有沒有指定的文件。

4.2 、 NoClassDefFoundError

在JavaDoc中對NoClassDefFoundError的產生可能的狀況就是使用new關鍵字、屬性引用某個類、繼承了某個接口或者類,以及方法的某個參數中引用了某個類,這時就會觸發JVM或者類加載器實例嘗試加載類型的定義,可是該定義卻沒有找到,影響了執行路徑。換句話說,在編譯時這個類是可以被找到的,可是在執行時卻沒有找到。

解決這個錯誤的方法就是確保每一個類引用的類都在當前的classpath下面。

4.3 、 UnsatisfiedLinkError

該錯誤一般是在 JVM 啓動的時候,若是 JVM 中的某個 lib 刪除了,就有可能報這個錯誤。

public class UnsatisfiedLinkErrorTest
{
    public native void nativeMethod();
    static {
        System.loadLibrary("NoLib");
    }
    public static void main(String[] args) {
        new UnsatisfiedLinkErrorTest().nativeMethod();  //解析native標識的方法時JVM找不到對應的庫文件
    }
}

這裏寫圖片描述

4.4 、 ClassCastException

該錯誤一般出現強制類型轉換時出現這個錯誤。

public class ClassCastExceptionTest
{
    public static Map m = new HashMap(){
        {
            put("a", "2");
        }
    };
    public static void main(String[] args) {
        Integer integer = (Integer) m.get("a"); //將m強制轉換成Integer類型
        System.out.println(integer);
    }
}

這裏寫圖片描述

注意:JVM 在作類型轉換時的規則:

  • 對於普通對象,對象必須是目標類的實例或目標類的子類的實例。若是目標類是接口,那麼會把它看成實現了該接口的一個子類。

  • 對於數組類型,目標類必須是數組類型或 java.lang.Object、java.lang.Cloneable、java.io.Serializable。

若是不知足上面的規則,JVM 就會報錯,有兩種方式可避免錯誤:

  • 在容器類型中顯式的指明這個容器所包含的對象類型。

  • 先經過 instanceof 檢查是否是目標類型,而後再進行強制類型的轉換。

上面代碼中改爲以下就能夠避免錯誤了:

這裏寫圖片描述

4.5 、 ExceptionInInitializerError

public class ExceptionInInitializerErrorTest
{
    public static Map m = new HashMap(){{
        m.put("a", "2");
    }};
    public static void main(String[] args) {
        Integer integer = (Integer) m.get("a");
        System.out.println(integer);
    }
}

這裏寫圖片描述

在初始化這個類時,給靜態屬性 m 賦值時出現了異常致使拋出錯誤 ExceptionInInitializerError。

4.6 NoSuchMethodError

NoSuchMethodError表明這個類型確實存在,可是一個不正確的版本被加載了。爲了解決這個問題咱們可使用 ‘­verbose:class’ 來判斷該JVM加載的究竟是哪一個版本。

4.7 LinkageError

有時候事情會變得更糟,和 ClassCastException 本質同樣,加載自不一樣位置的相同類在同一段邏輯(好比:方法)中交互時,會出現 LinkageError 。

LinkageError 須要觀察哪一個類被不一樣的類加載器加載了,在哪一個方法或者調用處發生(交匯)的,而後才能想解決方法,解決方法無外乎兩種。第一,仍是不一樣的類加載器加載,可是相互再也不交匯影響,這裏須要針對發生問題的地方作一些改動,好比更換實現方式,避免出現上述問題;第二,衝突的類須要由一個Parent類加載器進行加載。LinkageErrorClassCastException 本質是同樣的,加載自不一樣類加載器的類型,在同一個類的方法或者調用中出現,若是有轉型操做那麼就會拋 ClassCastException ,若是是直接的方法調用處的參數或者返回值解析,那麼就會產生 LinkageError 。

五、經常使用的 ClassLoader 分析

。。參見書籍《深刻分析Java Web技術內幕》

六、如何實現本身的 ClassLoader

ClassLoader 可以完成的事情有如下狀況:

  • 在自定義路徑下查找自定義的class類文件。

  • 對咱們本身要加載的類作特殊處理。

  • 能夠定義類的實現機制。

雖然在絕大多數狀況下,系統默認提供的類加載器實現已經能夠知足需求。可是在某些狀況下,您仍是須要爲應用開發出本身的類加載器。好比您的應用經過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼通過了加密處理。這個時候您就須要本身的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出要在 Java 虛擬機中運行的類來。

定義自已的類加載器分爲兩步:
一、繼承java.lang.ClassLoader
二、重寫父類的findClass方法

6.1 、文件系統類加載器

加載存儲在文件系統上的 Java 字節代碼。

public class FileSystemClassLoader extends ClassLoader
{
    private String rootDir;

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

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null){
            throw new ClassNotFoundException();
        }
        else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        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;
            while ((bytesNumRead = ins.read(buffer)) != -1){
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }
}

類 FileSystemClassLoader繼承自類java.lang.ClassLoader。java.lang.ClassLoader類的方法loadClass()封裝了前面提到的代理模式的實現。該方法會首先調用 findLoadedClass()方法來檢查該類是否已經被加載過;若是沒有加載過的話,會調用父類加載器的loadClass()方法來嘗試加載該類;若是父類加載器沒法加載該類的話,就調用 findClass()方法來查找該類。所以,爲了保證類加載器都正確實現代理模式,在開發本身的類加載器時,最好不要覆寫 loadClass()方法,而是覆寫findClass()方法。

類 FileSystemClassLoader的 findClass()方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文
件),而後讀取該文件內容,最後經過 defineClass()方法來把這些字節代碼轉換成 java.lang.Class類的實例。

6.2 、 網絡類加載器

一個網絡類加載器來講明如何經過類加載器來實現組件的動態更新。即基本的場景是:Java 字節代碼(.class)文件存放在服務器上,客戶端經過網絡的方式獲取字節代碼並執行。當有版本更新的時候,只須要替換掉服務
器上保存的文件便可。經過類加載器能夠比較簡單的實現這種需求。

類 NetworkClassLoader 負責經過網絡下載 Java 類字節代碼並定義出 Java 類。它的實現與FileSystemClassLoader 相似。在經過 NetworkClassLoader 加載了某個版本的類以後,通常有兩種作法來使用它。第一種作法是使用 Java 反射 API。另一種作法是使用接口。須要注意的是,並不能直接在客戶端代碼中引用從服務器上下載的類,由於客戶端代碼的類加載器找不到這些類。使用 Java 反射 API 能夠直接調用 Java 類的
方法。而使用接口的作法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不一樣版本的類。在客戶端經過相同的接口來使用這些實現類。

網絡類加載器的代碼:ClassLoader

七、類加載器與Web容器

對於運行在 Java EE™容器中的 Web 應用來講,類加載器的實現方式與通常的 Java 應用有所不一樣。不一樣的 Web 容器的實現方式也會有所不一樣。以 Apache Tomcat 來講,每一個Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不一樣的是它是首先嚐試去加載某個類,若是找不到再代理給父類加載器。這與通常類加載器的順序是相反的。這是 Java Servlet 規範中的推薦作法,其目的是使得Web 應用本身的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍以內的。這也是爲了保證 Java 核心庫的類型安全。

絕大多數狀況下,Web 應用的開發人員不須要考慮與類加載器相關的細節。下面給出幾條簡單的原則:

  • 每一個 Web 應用本身的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。

  • 多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由全部 Web 應用共享的目錄下面。

  • 當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確

八、總結

本篇文章詳細深刻的介紹了 ClassLoader 的工做機制,還寫了如何本身實現所需的 ClassLoader 。


參考資料

一、深度分析 Java 的 ClassLoader 機制(源碼級別)

二、深刻淺出ClassLoader

三、深刻探討 Java 類加載器

四、深刻分析Java ClassLoader原理

五、《深刻分析 Java Web 技術內幕》修訂版 —— 深刻分析 ClassLoader 工做機制

相關文章
相關標籤/搜索