深刻理解Java類加載器(ClassLoader)

【版權申明】未經博主贊成,謝絕轉載!(請尊重原創,博主保留追究權)

本篇博文主要是探討類加載器,同時在本篇中列舉的源碼都基於Java8版本,不一樣的版本可能有些許差別。主要內容以下java

類加載的機制的層次結構

每一個編寫的".java"拓展名類文件都存儲着須要執行的程序邏輯,這些".java"文件通過Java編譯器編譯成拓展名爲".class"的文件,".class"文件中保存着Java代碼經轉換後的虛擬機指令,當須要使用某個類時,虛擬機將會加載它的".class"文件,並建立對應的class對象,將class文件加載到虛擬機的內存,這個過程稱爲類加載,這裏咱們須要瞭解一下類加載的過程,以下:mysql

  • 加載:類加載過程的一個階段:經過一個類的徹底限定查找此類字節碼文件,並利用字節碼文件建立一個Class對象sql

  • 驗證:目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。數據庫

  • 準備:爲類變量(即static修飾的字段變量)分配內存而且設置該類變量的初始值即0(如static int i=5;這裏只將i初始化爲0,至於5的值將在初始化時賦值),這裏不包含用final修飾的static,由於final在編譯的時候就會分配了,注意這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中。api

  • 解析:主要將常量池中的符號引用替換爲直接引用的過程。符號引用就是一組符號來描述目標,能夠是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析(這裏涉及到字節碼變量的引用,如需更詳細瞭解,可參考《深刻Java虛擬機》)。數組

  • 初始化:類加載最後階段,若該類具備超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量(如前面只初始化了默認值的static變量將會在這個階段賦值,成員變量也將被初始化)。緩存

這即是類加載的5個過程,而類加載器的任務是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,而後轉換爲一個與目標類對應的java.lang.Class對象實例,在虛擬機提供了3種類加載器,引導(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),下面分別介紹安全

##啓動(Bootstrap)類加載器
啓動類加載器主要加載的是JVM自身須要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必因爲虛擬機是按照文件名識別加載jar包的,如rt.jar,若是文件名不被虛擬機識別,即便把jar包丟到lib目錄下也是沒有做用的(出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類)。網絡

##擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者能夠直接使用標準擴展類加載器。app

//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;
 }複製代碼

系統(System)類加載器

也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。它負責加載系統類路徑java -classpath-D java.class.path 指定路徑下的類庫,也就是咱們常常用到的classpath路徑,開發者能夠直接使用系統類加載器,通常狀況下該類加載是程序中默認的類加載器,經過ClassLoader#getSystemClassLoader()方法能夠獲取到該類加載器。
  在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,須要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象,並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面咱們進一步瞭解它。  

理解雙親委派模式

雙親委派模式工做原理

雙親委派模式要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器,請注意雙親委派模式中的父子關係並不是一般所說的類繼承關係,而是採用組合關係來複用父類加載器的相關代碼,類加載器間的關係以下:

雙親委派模式是在Java 1.2後引入的,其工做原理的是,若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行,若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式,即每一個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子本身想辦法去完成,這不就是傳說中的實力坑爹啊?那麼採用這種模式有啥用呢?

雙親委派模式優點

採用雙親委派模式的是好處是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係,經過這種層級關能夠避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設經過網絡傳遞一個名爲java.lang.Integer的類,經過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心Java API發現這個名字的類,發現該類已被加載,並不會從新加載網絡傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣即可以防止核心API庫被隨意篡改。可能你會想,若是咱們在classpath路徑下自定義一個名爲java.lang.SingleInterge類(該類是胡編的)呢?該類並不存在java.lang中,通過雙親委託模式,傳遞到啓動類加載器中,因爲父類加載器路徑下並無該類,因此不會加載,將反向委託給子類加載器加載,最終會經過系統類加載器加載該類。可是這樣作是不容許,由於java.lang是核心API包,須要訪問權限,強制加載將會報出以下異常

java.lang.SecurityException: Prohibited package name: java.lang複製代碼

因此不管如何都沒法加載成功的。下面咱們從代碼層面瞭解幾個Java中定義的類加載器及其雙親委派模式的實現,它們類圖關係以下

從圖能夠看出頂層的類加載器是ClassLoader類,它是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器),這裏咱們主要介紹ClassLoader中幾個比較重要的方法。

  • loadClass(String)

    該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2以後再也不建議用戶重寫但用戶能夠直接調用該方法,loadClass()方法是ClassLoader類本身實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼以下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數表明是否生成class對象的同時進行解析相關操做。

    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
                        // 若是都沒有找到,則經過自定義實現的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;
            }
        }複製代碼

    正如loadClass方法所展現的,當類加載請求到來時,先從緩存中查找該類對象,若是存在直接返回,若是不存在則交給該類加載去的父加載器去加載,假若沒有父加載則交給頂級啓動類加載器去加載,最後假若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也能夠知道若是不想從新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載本身指定的類,那麼咱們能夠直接使用this.getClass().getClassLoder.loadClass("className"),這樣就能夠直接調用ClassLoader的loadClass方法獲取到class對象。

  • findClass(String)
    在JDK1.2以前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗後,則會調用本身的findClass()方法來完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委託模式。須要注意的是ClassLoader類中並無實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法一般是和defineClass方法一塊兒使用的(稍後會分析),ClassLoader類中findClass()方法源碼以下:

    //直接拋出異常
      protected Class<?> findClass(String name) throws ClassNotFoundException {
              throw new ClassNotFoundException(name);
      }複製代碼
  • defineClass(byte[] b, int off, int len)
    defineClass()方法是用來將byte字節流解析成JVM可以識別的Class對象(ClassLoader中已實現該方法邏輯),經過這個方法不只可以經過class文件實例化class對象,也能夠經過其餘方式實例化class對象,如經過網絡接收一個類的字節碼,而後轉換爲byte字節流建立對應的Class對象,defineClass()方法一般與findClass()方法一塊兒使用,通常狀況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,而後調用defineClass()方法生成類的Class對象,簡單例子以下:

    protected Class
        
        
        
    
        findClass(String name) throws ClassNotFoundException {
            // 獲取類的字節數組
            byte[] classData = getClassData(name);  
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                //使用defineClass生成class對象
                return defineClass(name, classData, 0, classData.length);
            }
        }複製代碼

    須要注意的是,若是直接調用defineClass()方法生成類的Class對象,這個類的Class對象並無解析(也能夠理解爲連接階段,畢竟解析是連接的最後一步),其解析操做須要等待初始化階段進行。

  • resolveClass(Class≺?≻ c)
    使用該方法可使用類的Class對象建立完成也同時被解析。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用。

上述4個方法是ClassLoader類中的比較重要的方法,也是咱們可能會常常用到的方法。接看SercureClassLoader擴展了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的訪問權限)的方法,通常咱們不會直接跟這個類打交道,更可能是與它的子類URLClassLoader有所關聯,前面說過,ClassLoader是一個抽象類,不少方法是空的沒有實現,好比 findClass()、findResource()等。而URLClassLoader這個實現類爲這些方法提供了具體的實現,並新增了URLClassPath類協助取得Class字節碼流等功能,在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URLClassLoader類,這樣就能夠避免本身去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔,下面是URLClassLoader的類圖(利用IDEA生成的類圖)

從類圖結構看出URLClassLoader中存在一個URLClassPath類,經過這個類就能夠找到要加載的字節碼流,也就是說URLClassPath類負責找到要加載的字節碼,再讀取成字節流,最後經過defineClass()方法建立類的Class對象。從URLClassLoader類的結構圖能夠看出其構造方法都有一個必須傳遞的參數URL[],該參數的元素是表明字節碼文件的路徑,換句話說在建立URLClassLoader對象時必需要指定這個類加載器的到那個目錄下找class文件。同時也應該注意URL[]也是URLClassPath類的必傳參數,在建立URLClassPath對象時,會根據傳遞過來的URL數組中的路徑判斷是文件仍是jar包,而後根據不一樣的路徑建立FileLoader或者JarLoader或默認Loader類去加載相應路徑下的class文件,而當JVM調用findClass()方法時,就由這3個加載器中的一個將class文件的字節碼流加載到內存中,最後利用字節碼流建立類的class對象。請記住,若是咱們在定義類加載器時選擇繼承ClassLoader類而非URLClassLoader,必須手動編寫findclass()方法的加載邏輯以及獲取字節碼流的邏輯。瞭解完URLClassLoader後接着看看剩餘的兩個類加載器,即拓展類加載器ExtClassLoader和系統類加載器AppClassLoader,這兩個類都繼承自URLClassLoader,是sun.misc.Launcher的靜態內部類。sun.misc.Launcher主要被系統用於啓動主應用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher建立的,其類主要類結構以下:

它們間的關係正如前面所闡述的那樣,同時咱們發現ExtClassLoader並無重寫loadClass()方法,這足矣說明其遵循雙親委派模式,而AppClassLoader重載了loadCass()方法,但最終調用的仍是父類loadClass()方法,所以依然遵照雙親委派模式,重載方法源碼以下:

/**
  * Override loadClass 方法,新增包權限檢測功能
  */
 public Class loadClass(String name, boolean resolve)
     throws ClassNotFoundException
 {
     int i = name.lastIndexOf('.');
     if (i != -1) {
         SecurityManager sm = System.getSecurityManager();
         if (sm != null) {
             sm.checkPackageAccess(name.substring(0, i));
         }
     }
     //依然調用父類的方法
     return (super.loadClass(name, resolve));
 }複製代碼

其實不管是ExtClassLoader仍是AppClassLoader都繼承URLClassLoader類,所以它們都遵照雙親委託模型,這點是毋庸置疑的。ok~,到此咱們對ClassLoader、URLClassLoader、ExtClassLoader、AppClassLoader以及Launcher類間的關係有了比較清晰的瞭解,同時對一些主要的方法也有必定的認識,這裏並無對這些類的源碼進行詳細的分析,畢竟沒有那個必要,由於咱們主要弄得類與類間的關係和經常使用的方法同時搞清楚雙親委託模式的實現過程,爲編寫自定義類加載器作鋪墊就足夠了。ok~,前面出現了不少父類加載器的說法,但每一個類加載器的父類究竟是誰,一直沒有闡明,下面咱們就經過代碼驗證的方式來闡明這答案。

類加載器間的關係

咱們進一步瞭解類加載器間的關係(並不是指繼承關係),主要能夠分爲如下4點

  • 啓動類加載器,由C++實現,沒有父類。

  • 拓展類加載器(ExtClassLoader),由Java語言實現,父類加載器爲null

  • 系統類加載器(AppClassLoader),由Java語言實現,父類加載器爲ExtClassLoader

  • 自定義類加載器,父類加載器確定爲AppClassLoader。

下面咱們經過程序來驗證上述闡述的觀點

/** * Created by zejian on 2017/6/18. * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創] */
//自定義ClassLoader,完整代碼稍後分析
class FileClassLoader extends ClassLoader{
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    // 編寫獲取類的字節碼並建立class對象的邏輯
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
       //...省略邏輯代碼
    }
    //編寫讀取字節流的方法
    private byte[] getClassData(String className) {
        // 讀取類文件的字節
        //省略代碼....
    }
}

public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {

             FileClassLoader loader1 = new FileClassLoader(rootDir);

              System.out.println("自定義類加載器的父加載器: "+loader1.getParent());
              System.out.println("系統默認的AppClassLoader: "+ClassLoader.getSystemClassLoader());
              System.out.println("AppClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent());
              System.out.println("ExtClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent().getParent());

            /** 輸出結果: 自定義類加載器的父加載器: sun.misc.Launcher$AppClassLoader@29453f44 系統默認的AppClassLoader: sun.misc.Launcher$AppClassLoader@29453f44 AppClassLoader的父類加載器: sun.misc.Launcher$ExtClassLoader@6f94fa3e ExtClassLoader的父類加載器: null */

    }
}複製代碼

代碼中,咱們自定義了一個FileClassLoader,這裏咱們繼承了ClassLoader而非URLClassLoader,所以須要本身編寫findClass()方法邏輯以及加載字節碼的邏輯,關於自定義類加載器咱們稍後會分析,這裏僅須要知道FileClassLoader是自定義加載器便可,接着在main方法中,經過ClassLoader.getSystemClassLoader()獲取到系統默認類加載器,經過獲取其父類加載器及其父父類加載器,同時還獲取了自定義類加載器的父類加載器,最終輸出結果正如咱們所預料的,AppClassLoader的父類加載器爲ExtClassLoader,而ExtClassLoader沒有父類加載器。若是咱們實現本身的類加載器,它的父加載器都只會是AppClassLoader。這裏咱們不妨看看Lancher的構造器源碼

public Launcher() {
        // 首先建立拓展類加載器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }

        // Now create the class loader to use to launch the application
        try {
            //再建立AppClassLoader並把extcl做爲父加載器傳遞給AppClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }

        //設置線程上下文類加載器,稍後分析
        Thread.currentThread().setContextClassLoader(loader);
//省略其餘不必的代碼......
        }
    }複製代碼

顯然Lancher初始化時首先會建立ExtClassLoader類加載器,而後再建立AppClassLoader並把ExtClassLoader傳遞給它做爲父類加載器,這裏還把AppClassLoader默認設置爲線程上下文類加載器,關於線程上下文類加載器稍後會分析。那ExtClassLoader類加載器爲何是null呢?看下面的源碼建立過程就明白,在建立ExtClassLoader強制設置了其父加載器爲null。

//Lancher中建立ExtClassLoader
extcl = ExtClassLoader.getExtClassLoader();

//getExtClassLoader()方法
public static ExtClassLoader getExtClassLoader() throws IOException{

  //........省略其餘代碼 
  return new ExtClassLoader(dirs);                     
  // .........
}

//構造方法
public ExtClassLoader(File[] dirs) throws IOException {
   //調用父類構造URLClassLoader傳遞null做爲parent
   super(getExtURLs(dirs), null, factory);
}

//URLClassLoader構造
public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {複製代碼

顯然ExtClassLoader的父類爲null,而AppClassLoader的父加載器爲ExtClassLoader,全部自定義的類加載器其父加載器只會是AppClassLoader,注意這裏所指的父類並非Java繼承關係中的那種父子關係。

類與類加載器

類與類加載器

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

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

也就是說,在JVM中,即便這個兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不一樣,那麼這兩個類對象也是不相等的,這是由於不一樣的ClassLoader實例對象都擁有不一樣的獨立的類名稱空間,因此加載的class對象也會存在不一樣的類名空間中,但前提是覆寫loadclass方法,從前面雙親委派模式對loadClass()方法的源碼分析中能夠知,在方法第一步會經過Class<?> c = findLoadedClass(name);從緩存查找,類名完整名稱相同則不會再次被加載,所以咱們必須繞過緩存查詢才能從新加載class對象。固然也可直接調用findClass()方法,這樣也避免從緩存查找,以下

String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//建立兩個不一樣的自定義類加載器實例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//經過findClass建立類的Class對象
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");

System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());

/** * 直接調用findClass方法輸出結果: * findClass->obj1:723074861 findClass->obj2:895328852 生成不一樣的實例 */複製代碼

若是調用父類的loadClass方法,結果以下,除非重寫loadClass()方法去掉緩存查找步驟,不過如今通常都不建議重寫loadClass()方法。

//直接調用父類的loadClass()方法
Class<?> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class<?> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");

//不一樣實例對象的自定義類加載器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系統類加載器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());

/** * 直接調用loadClass方法的輸出結果,注意並無重寫loadClass方法 * loadClass->obj1:1872034366 loadClass->obj2:1872034366 Class-> obj3:1872034366 都是同一個實例 */複製代碼

因此若是不從緩存查詢相同徹底類名的class對象,那麼只有ClassLoader的實例對象不一樣,同一字節碼文件建立的class對象天然也不會相同。

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

所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內存的方式,顯示加載指的是在代碼中經過調用ClassLoader加載class對象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加載class對象。而隱式加載則是不直接在代碼中調用ClassLoader的方法加載class對象,而是經過虛擬機自動加載到內存中,如在加載某個類的class文件時,該類的class文件中引用了另一個類的對象,此時額外引用的類將經過JVM自動加載到內存中。在平常開發以上兩種方式通常會混合使用,這裏咱們知道有這麼回事便可。

編寫本身的類加載器

經過前面的分析可知,實現自定義類加載器須要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則須要本身重寫findClass()方法並編寫加載邏輯,繼承URLClassLoader則能夠省去編寫findClass()方法以及class文件加載轉換成字節碼流的代碼。那麼編寫自定義類加載器的意義何在呢?

  • 當class文件不在ClassPath路徑下,默認系統類加載器沒法找到該class文件,在這種狀況下咱們須要實現一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象。

  • 當一個class文件是經過網絡傳輸而且可能會進行相應的加密操做時,須要先對class文件進行相應的解密後再加載到JVM內存中,這種狀況下也須要編寫自定義的ClassLoader並實現相應的邏輯。

  • 當須要實現熱部署功能時(一個class文件經過不一樣的類加載器產生不一樣class對象從而實現熱部署功能),須要實現自定義ClassLoader的邏輯。

自定義File類加載器

這裏咱們繼承ClassLoader實現自定義的特定路徑下的文件類加載器並加載編譯後DemoObj.class,源碼代碼以下

public class DemoObj {
    @Override
    public String toString() {
        return "I am DemoObj";
    }
}複製代碼
package com.zejian.classloader;

import java.io.*;

/** * Created by zejian on 2017/6/21. */
public class FileClassLoader extends ClassLoader {
    private String rootDir;

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

    /** * 編寫findClass方法的邏輯 * @param name * @return * @throws ClassNotFoundException */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 獲取類的class文件字節數組
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class對象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /** * 編寫獲取class文件並轉換爲字節碼流的邏輯 * @param className * @return */
    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 (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /** * 類文件的徹底路徑 * @param className * @return */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //建立自定義文件類加載器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //加載指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //輸出結果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}複製代碼

顯然咱們經過getClassData()方法找到class文件並轉換爲字節流,並重寫findClass()方法,利用defineClass()方法建立了類的class對象。在main方法中調用了loadClass()方法加載指定路徑下的class文件,因爲啓動類加載器、拓展類加載器以及系統類加載器都沒法在其路徑下找到該類,所以最終將有自定義類加載器加載,即調用findClass()方法進行加載。若是繼承URLClassLoader實現,那代碼就更簡潔了,以下:

/** * Created by zejian on 2017/6/21. */
public class FileUrlClassLoader extends URLClassLoader {

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

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

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


    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //建立自定義文件類加載器
        File file = new File(rootDir);
        //File to URI
        URI uri=file.toURI();
        URL[] urls={uri.toURL()};

        FileUrlClassLoader loader = new FileUrlClassLoader(urls);

        try {
            //加載指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //輸出結果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}複製代碼

很是簡潔除了須要重寫構造器外無需編寫findClass()方法及其class文件的字節流轉換邏輯。

自定義網絡類加載器

自定義網絡類加載器,主要用於讀取經過網絡傳遞的class文件(在這裏咱們省略class文件的解密過程),並將其轉換成字節流生成對應的class對象,以下

/** * Created by zejian on 2017/6/21. */
public class NetClassLoader extends ClassLoader {

    private String url;//class文件的URL

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

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

    /** * 從網絡獲取class文件 * @param className * @return */
    private byte[] getClassDataFromNet(String className) {
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            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 (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

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

}複製代碼

比較簡單,主要是在獲取字節碼流時的區別,從網絡直接獲取到字節流再轉車字節數組而後利用defineClass方法建立class對象,若是繼承URLClassLoader類則和前面文件路徑的實現是相似的,無需擔憂路徑是filePath仍是Url,由於URLClassLoader內的URLClassPath對象會根據傳遞過來的URL數組中的路徑判斷是文件仍是jar包,而後根據不一樣的路徑建立FileLoader或者JarLoader或默認類Loader去讀取對於的路徑或者url下的class文件。

熱部署類加載器

所謂的熱部署就是利用同一個class文件不一樣的類加載器在內存建立出兩個不一樣的class對象(關於這點的緣由前面已分析過,即利用不一樣的類加載實例),因爲JVM在加載類以前會檢測請求的類是否已加載過(即在loadClass()方法中調用findLoadedClass()方法),若是被加載過,則直接從緩存獲取,不會從新加載。注意同一個類加載器的實例和同一個class文件只能被加載器一次,屢次加載將報錯,所以咱們實現的熱部署必須讓同一個class文件能夠根據不一樣的類加載器重複加載,以實現所謂的熱部署。實際上前面的實現的FileClassLoader和FileUrlClassLoader已具有這個功能,但前提是直接調用findClass()方法,而不是調用loadClass()方法,由於ClassLoader中loadClass()方法體中調用findLoadedClass()方法進行了檢測是否已被加載,所以咱們直接調用findClass()方法就能夠繞過這個問題,固然也能夠從新loadClass方法,但強烈不建議這麼幹。利用FileClassLoader類測試代碼以下:

public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //建立自定義文件類加載器
        FileClassLoader loader = new FileClassLoader(rootDir);
        FileClassLoader loader2 = new FileClassLoader(rootDir);

        try {
            //加載指定的class文件,調用loadClass()
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            Class<?> object2=loader2.loadClass("com.zejian.classloader.DemoObj");

            System.out.println("loadClass->obj1:"+object1.hashCode());
            System.out.println("loadClass->obj2:"+object2.hashCode());

            //加載指定的class文件,直接調用findClass(),繞過檢測機制,建立不一樣class對象。
            Class<?> object3=loader.findClass("com.zejian.classloader.DemoObj");
            Class<?> object4=loader2.findClass("com.zejian.classloader.DemoObj");

            System.out.println("loadClass->obj3:"+object3.hashCode());
            System.out.println("loadClass->obj4:"+object4.hashCode());

            /** * 輸出結果: * loadClass->obj1:644117698 loadClass->obj2:644117698 findClass->obj3:723074861 findClass->obj4:895328852 */

        } catch (Exception e) {
            e.printStackTrace();
        }
    }複製代碼

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

在Java應用中存在着不少服務提供者接口(Service Provider Interface,SPI),這些接口容許第三方爲它們提供實現,如常見的 SPI 有 JDBC、JNDI等,這些 SPI 的接口屬於 Java 核心庫,通常存在rt.jar包中,由Bootstrap類加載器加載,而 SPI 的第三方實現代碼則是做爲Java應用所依賴的 jar 包被存放在classpath路徑下,因爲SPI接口中的代碼常常須要加載具體的第三方實現類並調用其相關方法,但SPI的核心接口類是由引導類加載器來加載的,而Bootstrap類加載器沒法直接加載SPI的實現類,同時因爲雙親委派模式的存在,Bootstrap類加載器也沒法反向委託AppClassLoader加載器SPI的實現類。在這種狀況下,咱們就須要一種特殊的類加載器來加載第三方的類庫,而線程上下文類加載器就是很好的選擇。
    線程上下文類加載器(contextClassLoader)是從 JDK 1.2 開始引入的,咱們能夠經過java.lang.Thread類中的getContextClassLoader()setContextClassLoader(ClassLoader cl)方法來獲取和設置線程的上下文類加載器。若是沒有手動設置上下文類加載器,線程將繼承其父線程的上下文類加載器,初始線程的上下文類加載器是系統類加載器(AppClassLoader),在線程中運行的代碼能夠經過此類加載器來加載類和資源,以下圖所示,以jdbc.jar加載爲例

從圖可知rt.jar核心包是有Bootstrap類加載器加載的,其內包含SPI核心接口類,因爲SPI中的類常常須要調用外部實現類的方法,而jdbc.jar包含外部實現類(jdbc.jar存在於classpath路徑)沒法經過Bootstrap類加載器加載,所以只能委派線程上下文類加載器把jdbc.jar中的實現類加載到內存以便SPI相關類使用。顯然這種線程上下文類加載器的加載方式破壞了「雙親委派模型」,它在執行過程當中拋棄雙親委派加載鏈模式,使程序能夠逆向使用類加載器,固然這也使得Java類加載器變得更加靈活。爲了進一步證明這種場景,不妨看看DriverManager類的源碼,DriverManager是Java核心rt.jar包中的類,該類用來管理不一樣數據庫的實現驅動即Driver,它們都實現了Java核心包中的java.sql.Driver接口,如mysql驅動包中的com.mysql.jdbc.Driver,這裏主要看看如何加載外部實現類,在DriverManager初始化時會執行以下代碼

//DriverManager是Java核心包rt.jar的類
public class DriverManager {
    //省略沒必要要的代碼
    static {
        loadInitialDrivers();//執行該方法
        println("JDBC DriverManager initialized");
    }

//loadInitialDrivers方法
 private static void loadInitialDrivers() {
     sun.misc.Providers()
     AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //加載外部的Driver的實現類
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              //省略沒必要要的代碼......
            }
        });
    }複製代碼

在DriverManager類初始化時執行了loadInitialDrivers()方法,在該方法中經過ServiceLoader.load(Driver.class);去加載外部實現的驅動類,ServiceLoader類會去讀取mysql的jdbc.jar下META-INF文件的內容,以下所示

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

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驅動

//不建議使用該方式註冊驅動類
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 經過java庫獲取數據庫鏈接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");複製代碼

而是直接去掉註冊步驟,以下便可

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 經過java庫獲取數據庫鏈接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");複製代碼

這樣ServiceLoader會幫助咱們處理一切,並最終經過load()方法加載,看看load()方法實現

public static ServiceLoader load(Class service) {
     //經過線程上下文類加載器加載
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }複製代碼

很明顯了確實經過線程上下文類加載器加載的,實際上核心包的SPI類對外部實現類的加載都是基於線程上下文類加載器執行的,經過這種方式實現了Java核心代碼內部去調用外部實現類。咱們知道線程上下文類加載器默認狀況下就是AppClassLoader,那爲何不直接經過getSystemClassLoader()獲取類加載器來加載classpath路徑下的類的呢?實際上是可行的,但這種直接使用getSystemClassLoader()方法獲取AppClassLoader加載類有一個缺點,那就是代碼部署到不一樣服務時會出現問題,如把代碼部署到Java Web應用服務或者EJB之類的服務將會出問題,由於這些服務使用的線程上下文類加載器並不是AppClassLoader,而是Java Web應用服自家的類加載器,類加載器不一樣。,因此咱們應用該少用getSystemClassLoader()。總之不一樣的服務使用的可能默認ClassLoader是不一樣的,但使用線程上下文類加載器總能獲取到與當前程序執行相同的ClassLoader,從而避免沒必要要的問題。ok~.關於線程上下文類加載器暫且聊到這,前面闡述的DriverManager類,你們能夠自行看看源碼,相信會有更多的體會,另外關於ServiceLoader本篇並無過多的闡述,畢竟咱們主題是類加載器,但ServiceLoader是個很不錯的解耦機制,你們能夠自行查閱其相關用法。

ok~,本篇到此告一段落,若有誤處,歡迎留言,謝謝。

參考資料:
blog.csdn.net/yangcheng33…
ifeve.com/wp-content/…

《深刻理解JVM虛擬機》《深刻分析Java Web 技術內幕》

相關文章
相關標籤/搜索