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