一、ClassLoader概念
ClassLoader是用來動態的加載class文件到虛擬機中,並轉換成java.lang.class類的一個實例,每個這樣的實例用來表示一個java類,我們可以根據Class的實例得到該類的信息,並通過實例的newInstance()方法創建出該類的一個對象,除此之外,ClassLoader還負責加載Java應用所需的資源,如圖像文件和配置文件等。
ClassLoader類是一個抽象類。如果給定類的二進制名稱,那么類加載器會試圖查找或生成構成類定義的數據。一般策略是將名稱轉換為某個文件名,然后從文件系統讀取該名稱的“類文件”。ClassLoader類使用委托模型來搜索類和資源。每個 ClassLoader實例都有一個相關的父類加載器。需要查找類或資源時,ClassLoader實例會在試圖親自查找類或資源之前,將搜索類或資源的任務委托給其父類加載器。
注意:程序在啟動的時候,並不會一次性加載程序所要用的所有class文件,而是根據程序的需要,通過Java的類加載機制來動態加載某個class文件到內存中。
二、JVM平台提供三層classLoader
- Bootstrap classLoader:采用native code實現,是JVM的一部分,主要加載JVM自身工作需要的類,如java.lang.*、java.uti.*等; 這些類位於$JAVA_HOME/jre/lib/rt.jar。Bootstrap ClassLoader不繼承自ClassLoader,因為它不是一個普通的Java類,底層由C++編寫,已嵌入到了JVM內核當中,當JVM啟動后,Bootstrap ClassLoader也隨着啟動,負責加載完核心類庫后,並構造Extension ClassLoader和App ClassLoader類加載器。
- ExtClassLoader:擴展的class loader,加載位於$JAVA_HOME/jre/lib/ext目錄下的擴展jar。
- AppClassLoader:系統class loader,父類是ExtClassLoader,加載$CLASSPATH下的目錄和jar;它負責加載應用程序主函數類。
其體系結構圖如下:
如果要實現自己的類加載器,不管是實現抽象列ClassLoader,還是繼承URLClassLoader類,它的父加載器都是AppClassLoader,因為不管調用哪個父類加載器,創建的對象都必須最終調用getSystemClassLoader()作為父加載器,getSystemClassLoader()方法獲取到的正是AppClassLoader。
注意:Bootstrap classLoader並不屬於JVM的等級層次,它不遵守ClassLoader的加載規則,Bootstrap classLoader並沒有子類。
三、JVM加載class文件到內存有兩種方式
- 隱式加載:不通過在代碼里調用ClassLoader來加載需要的類,而是通過JVM來自動加載需要的類到內存,例如:當類中繼承或者引用某個類時,JVM在解析當前這個類不在內存中時,就會自動將這些類加載到內存中。
- 顯示加載:在代碼中通過ClassLoader類來加載一個類,例如調用this.getClass.getClassLoader().loadClass()或者Class.forName()。
四、ClassLoader加載類的過程
- 找到.class文件並把這個文件加載到內存中
- 字節碼驗證,Class類數據結構分析,內存分配和符號表的鏈接
- 類中靜態屬性和初始化賦值以及靜態代碼塊的執行
五、自定義類加載器
1、為何要自定義類加載器?
JVM提供的類加載器,只能加載指定目錄的jar和class,如果我們想加載其他位置的類或jar時,例如加載網絡上的一個class文件,默認的ClassLoader就不能滿足我們的需求了,所以需要定義自己的類加載器。
2、如何實現自定義的類加載器?
我們實現一個ClassLoader,並指定這個ClassLoader的加載路徑。有兩種方式:
方式一:繼承ClassLoader,重寫父類的findClass()方法,代碼如下:
1 import java.io.ByteArrayOutputStream; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.io.IOException; 5 public class PathClassLoader extends ClassLoader 6 { 7 public static final String drive = "d:/"; 8 public static final String fileType = ".class"; 9 10 public static void main(String[] args) throws Exception 11 { 12 PathClassLoader loader = new PathClassLoader(); 13 Class<?> objClass = loader.loadClass("HelloWorld", true); 14 Object obj = objClass.newInstance(); 15 System.out.println(objClass.getName()); 16 System.out.println(objClass.getClassLoader()); 17 System.out.println(obj.getClass().toString()); 18 } 19 20 public Class<?> findClass(String name) 21 { 22 byte[] data = loadClassData(name); 23 return defineClass(name, data, 0, data.length);// 將一個 byte 數組轉換為 Class// 類的實例 24 } 25 public byte[] loadClassData(String name) 26 { 27 FileInputStream fis = null; 28 byte[] data = null; 29 try 30 { 31 fis = new FileInputStream(new File(drive + name + fileType)); 32 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 33 int ch = 0; 34 while ((ch = fis.read()) != -1) 35 { 36 baos.write(ch); 37 } 38 data = baos.toByteArray(); 39 } catch (IOException e) 40 { 41 e.printStackTrace(); 42 } 43 return data; 44 } 45 }
在第13行,我們調用了父類的loadClass()方法,該方法使用指定的二進制名稱來加載類,下面是loadClass方法的源代碼:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 第一步先檢查這個類是否已經被加載 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //parent為父加載器 if (parent != null) { //將搜索類或資源的任務委托給其父類加載器 c = parent.loadClass(name, false); } else { //檢查該class是否被BootstrapClassLoader加載 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { //如果上述兩步均沒有找到加載的class,則調用findClass()方法 long t1 = System.nanoTime(); 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; } }
這個方法首先檢查指定class是否已經被加載,如果已被加載過,則調用resolveClass()方法鏈接指定的類,如果還未加載,則先將搜索類或資源的任務委托給其父類加載器,檢查該class是否被BootstrapClassLoader加載,如果上述兩步均沒有找到加載的class,則調用findClass()方法,在我們自定義的加載器中,我們重寫了findClass方法,去我們指定的路徑下加載class文件。
另外,我們自定義的類加載器沒有指定父加載器,在JVM規范中不指定父類加載器的情況下,默認采用系統類加載器即AppClassLoader作為其父加載器,所以在使用該自定義類加載器時,需要加載的類不能在類路徑中,否則的話根據雙親委派模型的原則,待加載的類會由系統類加載器加載。如果一定想要把自定義加載器需要加載的類放在類路徑中, 就要把自定義類加載器的父加載器設置為null。
方式二:繼承URLClassLoader類,然后設置自定義路徑的URL來加載URL下的類。
我們將指定的目錄轉換為URL路徑,然后重寫findClass方法。
六、實現類的熱部署
1、什么是類的熱部署?
所謂熱部署,就是在應用正在運行的時候升級軟件,不需要重新啟用應用。
對於Java應用程序來說,熱部署就是運行時更新Java類文件。在基於Java的應用服務器實現熱部署的過程中,類裝入器扮演着重要的角色。大多數基於Java的應用服務器,包括EJB服務器和Servlet容器,都支持熱部署。
類裝入器不能重新裝入一個已經裝入的類,但只要使用一個新的類裝入器實例,就可以將類再次裝入一個正在運行的應用程序。
2、如何實現Java類的熱部署
前面的分析,我們已經知道,JVM在加載類之前會檢查請求的類是否已經被加載過來,也就是要調用findLoadedClass方法查看是否能夠返回類實例。如果類已經加載過來,再調用loadClass會導致類沖突。
但是,JVM判斷一個類是否是同一個類有兩個條件:一是看這個類的完整類名是否一樣(包括包名),二是看加載這個類的ClassLoader加載器是否是同一個(既是是同一個ClassLoader類的兩個實例,加載同一個類也會不一樣)。
所以,要實現類的熱部署可以創建不同的ClassLoader的實例對象,然后通過這個不同的實例對象來加載同名的類。