天天看點

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

原文:https://blog.csdn.net/javazejian/article/details/72772461

深入了解(1)Java注解類型(@Annotation)

深入了解(2)Java枚舉類型(enum)

深入了解(3)Java類加載器(ClassLoader)

深入了解(4)Java類型資訊(Class對象)與反射機制

深入了解(5)Java記憶體模型(JMM)及volatile關鍵字

深入了解(6)Java并發AQS的共享鎖的實作(基于信号量Semaphore)

深入了解(7)Java無鎖CAS與Unsafe類及其并發包Atomic

深入了解(8)Java并發之synchronized實作原理

深入了解(9)Java基于并發AQS的(獨占鎖)重入鎖(ReetrantLock)及其Condition實作原理

深入了解(10)java并發之阻塞隊列LinkedBlockingQueue與ArrayBlockingQueue

文章目錄

  • 類加載的機制的層次結構
    • 啟動(Bootstrap)類加載器
    • 擴充(Extension)類加載器
    • 系統(System)類加載器
  • 了解雙親委派模式
    • 雙親委派模式工作原理
    • 雙親委派模式優勢
    • 類加載器間的關系
  • 類與類加載器
    • 類與類加載器
    • 了解class檔案的顯示加載與隐式加載的概念
  • 編寫自己的類加載器
    • 自定義File類加載器
    • 自定義網絡類加載器
    • 熱部署類加載器
  • 雙親委派模型的破壞者-線程上下文類加載器

類加載的機制的層次結構

每個編寫的”.java”拓展名類檔案都存儲着需要執行的程式邏輯,這些”.java”檔案經過Java編譯器編譯成拓展名為”.class”的檔案,”.class”檔案中儲存着Java代碼經轉換後的虛拟機指令,當需要使用某個類時,虛拟機将會加載它的”.class”檔案,并建立對應的class對象,将class檔案加載到虛拟機的記憶體,這個過程稱為類加載,這裡我們需要了解一下類加載的過程,如下:

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器
  • 加載:類加載過程的一個階段:通過一個類的完全限定查找此類位元組碼檔案,并利用位元組碼檔案建立一個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;
 }
12345678910111213141516171819
           

系統(System)類加載器

也稱應用程式加載器是指 Sun公司實作的

sun.misc.Launcher$AppClassLoader

。它負責加載系統類路徑

java -classpath

-D java.class.path

指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程式中預設的類加載器,通過

ClassLoader#getSystemClassLoader()

方法可以擷取到該類加載器。

  在Java的日常應用程式開發中,類的加載幾乎是由上述3種類加載器互相配合執行的,在必要時,我們還可以自定義類加載器,需要注意的是,Java虛拟機對class檔案采用的是按需加載的方式,也就是說當需要使用該類時才會将它的class檔案加載到記憶體生成class對象,而且加載某個類的class檔案時,Java虛拟機采用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面我們進一步了解它。

了解雙親委派模式

雙親委派模式工作原理

雙親委派模式要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器,請注意雙親委派模式中的父子關系并非通常所說的類繼承關系,而是采用組合關系來複用父類加載器的相關代碼,類加載器間的關系如下:

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

雙親委派模式是在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.lang1
           

是以無論如何都無法加載成功的。下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實作,它們類圖關系如下

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

從圖可以看出頂層的類加載器是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;
          }
      }1234567891011121314151617181920212223242526272829303132333435363738
               
    正如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);
    }1234
               
  • 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);
          }
      }12345678910
               
    需要注意的是,如果直接調用defineClass()方法生成類的Class對象,這個類的Class對象并沒有解析(也可以了解為連結階段,畢竟解析是連結的最後一步),其解析操作需要等待初始化階段進行。
  • resolveClass(Class≺?≻ c)

    使用該方法可以使用類的Class對象建立完成也同時被解析。前面我們說連結階段主要是對位元組碼進行驗證,為類變量配置設定記憶體并設定初始值同時将位元組碼檔案中的符号引用轉換為直接引用。

上述4個方法是ClassLoader類中的比較重要的方法,也是我們可能會經常用到的方法。接看SercureClassLoader擴充了 ClassLoader,新增了幾個與使用相關的代碼源(對代碼源的位置及其證書的驗證)和權限定義類驗證(主要指對class源碼的通路權限)的方法,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關聯,前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實作,比如 findClass()、findResource()等。而URLClassLoader這個實作類為這些方法提供了具體的實作,并新增了URLClassPath類協助取得Class位元組碼流等功能,在編寫自定義類加載器時,如果沒有太過于複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其擷取位元組碼流的方式,使自定義類加載器編寫更加簡潔,下面是URLClassLoader的類圖(利用IDEA生成的類圖)

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

從類圖結構看出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

建立的,其類主要類結構如下:

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

它們間的關系正如前面所闡述的那樣,同時我們發現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));
 }12345678910111213141516
           

其實無論是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());

            /**
            輸出結果:
                自定義類加載器的父加載器: [email protected]
                系統預設的AppClassLoader: [email protected]
                AppClassLoader的父類加載器: [email protected]
                ExtClassLoader的父類加載器: null
            */

    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344
           

代碼中,我們自定義了一個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);
//省略其他沒必要的代碼......
        }
    }123456789101112131415161718192021222324
           

顯然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) {1234567891011121314151617181920
           

顯然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
    生成不同的執行個體
  */1234567891011121314151617
           

如果調用父類的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
  都是同一個執行個體
*/1234567891011121314151617
           

是以如果不從緩存查詢相同完全類名的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";
    }
}123456
           
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();
        }
    }
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
           

顯然我們通過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();
        }
    }
}12345678910111213141516171819202122232425262728293031323334353637383940
           

非常簡潔除了需要重寫構造器外無需編寫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";
    }

}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
           

比較簡單,主要是在擷取位元組碼流時的差別,從網絡直接擷取到位元組流再轉車位元組數組然後利用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();
        }
    }123456789101112131415161718192021222324252627282930313233
           

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

​ 在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加載為例

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

從圖可知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);
              //省略不必要的代碼......
            }
        });
    }12345678910111213141516171819
           

在DriverManager類初始化時執行了loadInitialDrivers()方法,在該方法中通過

ServiceLoader.load(Driver.class);

去加載外部實作的驅動類,ServiceLoader類會去讀取mysql的jdbc.jar下META-INF檔案的内容,如下所示

深入了解(3)Java類加載器(ClassLoader)類加載的機制的層次結構了解雙親委派模式類與類加載器編寫自己的類加載器雙親委派模型的破壞者-線程上下文類加載器

而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.");
    }
}12345678910
           

從注釋可以看出平常我們使用

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", "[email protected]");
123456
           

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

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫擷取資料庫連接配接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "[email protected]");
1234
           

這樣ServiceLoader會幫助我們處理一切,并最終通過load()方法加載,看看load()方法實作

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

很明顯了确實通過線程上下文類加載器加載的,實際上核心包的SPI類對外部實作類的加載都是基于線程上下文類加載器執行的,通過這種方式實作了Java核心代碼内部去調用外部實作類。我們知道線程上下文類加載器預設情況下就是AppClassLoader,那為什麼不直接通過getSystemClassLoader()擷取類加載器來加載classpath路徑下的類的呢?其實是可行的,但這種直接使用getSystemClassLoader()方法擷取AppClassLoader加載類有一個缺點,那就是代碼部署到不同服務時會出現問題,如把代碼部署到Java Web應用服務或者EJB之類的服務将會出問題,因為這些服務使用的線程上下文類加載器并非AppClassLoader,而是Java Web應用服自家的類加載器,類加載器不同。,是以我們應用該少用getSystemClassLoader()。總之不同的服務使用的可能預設ClassLoader是不同的,但使用線程上下文類加載器總能擷取到與目前程式執行相同的ClassLoader,進而避免不必要的問題。ok~.關于線程上下文類加載器暫且聊到這,前面闡述的DriverManager類,大家可以自行看看源碼,相信會有更多的體會,另外關于ServiceLoader本篇并沒有過多的闡述,畢竟我們主題是類加載器,但ServiceLoader是個很不錯的解耦機制,大家可以自行查閱其相關用法。