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