Jvm知識學習(六)-類裝載器

類加載的機制的層次結構

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



  • 加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節碼文件,並利用字節碼文件創建一個Class對象

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

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

  • 解析:主要將常量池中的符號引用替換爲直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析(這裏涉及到字節碼變量的引用,如需更詳細瞭解,可參考《深入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指定位路徑中的類庫,開發者可以直接使用標準擴展類加載器。

 
//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
1

所以無論如何都無法加載成功的。下面我們從代碼層面瞭解幾個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;
          }
      }
    8

    正如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.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
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.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
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.
 * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創]
 */
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 <S> ServiceLoader<S> load(Class<S> 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是個很不錯的解耦機制,大家可以自行查閱其相關用法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章