第18次文章:JVM中的類加載機制

  • 2019 年 10 月 8 日
  • 筆記

這周介紹一下JVM中的類加載機制,主要是類加載器的層次結構,代理模式以及自定義類加載器。

一、類加載器的層次結構(樹狀結構)

1、引導類加載器(bootstrap class loader)

-主要用來加載java的核心庫,是用原生代碼C語言來實現的,並不繼承自java.lang.ClassLoader。

-加載擴展類和應用程序類加載器。並不指定他們的父類加載器。

2、擴展程序類(extensions class loader)

-用來加載java的擴展庫。java虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載java類。

-由sun.misc.Launcher$ExtClassLoader實現

3、應用程序類加載器(application class loader)

-它是根據java應用的類路徑(classpath,java.class.path),一般來說,java應用的類都是由它來完成加載的。

4、自定義類加載器

-開發人員可以通過繼承java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。

下面我們先簡單的測試一下幾個類加載器的層次結構:

public class Demo02 {    public static void main(String[] args) {    //應用程序類加載器jdk.internal.loader.ClassLoaders$AppClassLoader@2a33fae0    System.out.println(ClassLoader.getSystemClassLoader());    //擴展類加載器jdk.internal.loader.ClassLoaders$PlatformClassLoader@707f7052    System.out.println(ClassLoader.getSystemClassLoader().getParent());    //引導類加載器,使用原生代碼實現(C),所以此處無法獲取    System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());        System.out.println(System.getProperty("java.class.path"));    }  }

查看一下結果:

tips:

(1)首先我們獲取當前線程的類加載器,可以得到一個應用程序類加載器(AppClassLoader),然後獲取其父類加載器,得到一個擴展類加載器(PlatformClassLoader),最後再獲取擴展類加載器的父類,輸出為null,是因為擴展類加載器的父類為引導類加載器(Bootstrap class loader),同時,引導類加載器是使用原生代碼實現的,並不是繼承自java.lang.ClassLoader,所以在獲取其名稱時,無法在java環境中顯示。由此可以得到,在類加載器中,其層次結構為:自定義類加載器——>應用程序類加載器——>擴展類加載器——>引導類加載器。

(2)我們獲取當前類加載器的加載目錄「java.class.path」,可以看出,其加載目錄在當前工程文件下的bin目錄內。

二、類加載器的代理模式:

類加載器的代理模式是指:在加載指定的類的時候,當前類加載器並不直接加載這個類,而是交給其他類進行加載。下面我們介紹一種代理模式——雙親委託機制。

雙親委託機制:

-就是某個特定的類加載器在介紹加載類的請求時,首先將加載任務委託給父類加載器,依次追溯,直到最原始的父類,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。

-雙親委託機制是為了保證java核心庫的類型安全。這種機制就保證了用戶無法使用自己定義的java.lang.Object類的情況。

-類加載器除了用於加載類,也是安全的最基本的屏障。

我們舉一個簡單的實例,進行分析雙親委託機制的安全性:

首先,我們自己建立一個經常使用,但是卻從來沒有建過的類java.lang.String

package java.lang;  public class String {  public String toString() {    return "aaa";  }}

然後我們使用調用此類:

package com.peng.test;  public class Demo02 {    public static void main(String[] args) {    String a = "peng";    System.out.println(a.getClass().getClassLoader());    System.out.println(a.toString());      }  }

輸出結果:

tips:

(1)我們先關注一下結果,在自定義的String類中,我們是返回一個字符串「aaa」,而最後打印在控制台上的內容是我們重新定義的一個變量「peng」。所以類加載器在加載String類的時候,直接加載了java核心包(rt.jar)中的java.lang.String類,而不是我們自定義的java.lang.String類。

(2)導致這種結果的原因就是類加載機制中的雙親委派機制。當我們的系統類加載器獲取到String類的時候,首先會交給其父類擴展類加載器,然後又交給擴展類加載器的父類——引導類加載器。引導類加載器為最高層父類,所以,當一個類被加載的時候,首先是將其一層一層向上傳遞,最後交給引導類加載器,從引導類加載器開始進行加載。當引導類加載器獲取到java.lang.String類的時候,直接可以從java核心庫當中加載此類,所以不需要將此類向下傳遞加載。

(3)這種機制就確保了我們無法使用自定義的java核心庫中的類,保護了java核心庫的安全性。

(4)代理模式有很多種,雙親委託機制是代理模式的一種,也並不是所有的類加載器都採用雙親委託機制。比如說:Tomcat服務器類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。

三、自定義類加載器

自定義類加載器的流程:

-繼承:java.lang.ClassLoader

-首先檢查請求的類型是否已經被這個類加載器加載到命名空間中了,如果已經加載,直接返回;

-委派類加載請求給父類加載器,如果父類加載器能夠完成,則返回父類加載器加載的Class實例;

-調用本類加載器的findClass(…)方法,試圖獲取對應的位元組碼,如果獲取的到,則調用defineClass(…)導入類型到方法區;如果獲取不到對應的位元組碼或者其他原因失敗,返回異常給loadClass(…),loadClass(…)轉拋異常,終止加載過程。

下面,我們根據上述流程,寫一個具體的自定義文件系統類加載器:

/** * 自定義文件系統類加載器 * */public class FileSystemClassLoader extends ClassLoader {    private String rootDir;//根目錄  public FileSystemClassLoader(String rootDir) {    this.rootDir = rootDir;  }    @Override  protected Class<?> findClass(String name) throws ClassNotFoundException {    Class<?> c = findLoadedClass(name);//在已加載的類中查找name        //應該要先查詢有沒有加載過這個類,如果已經加載,則直接返回加載號的類。如果沒有,則加載新的類    if(c!=null) {      return c;    }else {//雙親委派機制,委派給父類進行加載      ClassLoader parent = this.getParent();//獲取該類的父類加載器      try {        c = parent.loadClass(name);//委派給父類加載      } catch (Exception e) {        // TODO: handle exception      }            if(c != null) {        return c;      }else {//如果父類加載器中也沒有將該類進行加載,則通過IO流,將該類別信息輸入,然後自定義此類        byte[] classData = getClassData(name);//獲取此類的信息        if(classData == null) {          throw new ClassNotFoundException();        }else {          c = defineClass(name, classData, 0, classData.length);//定義此類        }                return c;      }    }  }    /**   * 以位元組數組的方式獲取類信息   * @param className   * @return   */  private byte[] getClassData(String className) {    String classPath = this.rootDir + "/" + className.replace(".", "/") + ".class";        InputStream is = null;    ByteArrayOutputStream baos = new ByteArrayOutputStream();      try {//讀寫待加載類的文件      is = new FileInputStream(classPath);      byte[] buffer = new byte[1024];      int temp = 0;      while(-1 != (temp=is.read(buffer))) {        baos.write(buffer, 0, temp);      }      return baos.toByteArray();      } catch (Exception e) {      // TODO Auto-generated catch block      e.printStackTrace();      return null;    } finally {//關閉IO流      if(is != null) {        try {          is.close();        } catch (IOException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }      if(baos != null) {        try {          baos.close();        } catch (IOException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }    }  }  }

然後我們簡單的測試一下這個自定義文件系統類加載器:

package com.peng.test;  /** * 測試自定義的類加載器 FileSystemClassLoader */public class Demo03 {  public static void main(String[] args) throws ClassNotFoundException {    FileSystemClassLoader loader = new FileSystemClassLoader("G:/java學習/test");    FileSystemClassLoader loader2 = new FileSystemClassLoader("G:/java學習/test");        Class<?> c = loader.loadClass("com.peng.test.User");    Class<?> c2 = loader.loadClass("com.peng.test.User");    Class<?> c3 = loader2.loadClass("com.peng.test.User");        Class<?> c4 = loader2.loadClass("java.lang.String");    Class<?> c5 = loader2.loadClass("com.peng.test.Demo01");        System.out.println(c);    System.out.println(c.hashCode());    System.out.println(c2.hashCode());    System.out.println(c3.hashCode());//同一個類,被兩個類加載器加載的同一個類,JVM認不認為是相同的類        System.out.println(c4.hashCode());        System.out.println(c3.getClassLoader());//使用我們自定義的類加載器    System.out.println(c4.getClassLoader());//引導類加載器    System.out.println(c5.getClassLoader());//系統默認的類加載器AppClassLoader  }}

輸出結果如下:

tips:

(1)首先我們觀察對象c和c2,兩者是使用同一個文件系統類加載器,加載同一個類得到對象,所以兩個對象的hashcode相同,屬於同一個對象。

(2)但是我們觀察對象c3和c,c3使用了另一個類加載器loader2進行加載,加載的類和c加載的類是相同的,但是最後兩者的hashcode不同,代表了兩個不同的對象,這個結果證明被兩個類加載器加載的同一個類,JVM認為是不同的類。

(3)我們再創建兩個對象c4和c5,分別加載核心類「java.lang.String」和當前工程文件中的類「com.peng.test.Demo01」,分別獲得c3、c4、c5的類加載器,並輸出到控制台上。可以發現c3使用的是我們自定義的文件系統類加載器,c4依舊使用的是引導類加載器,c5使用的是應用程序類加載器。因為c5中加載的Demo01對象屬於此工程文件中的一個文件,所以我們的主程序Demo03在加載的時候,就已經使用應用程序類加載器將其加載在JVM中了,並不需要使用自定義文件系統類加載器。