第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中了,並不需要使用自定義文件系統類載入器。