在上一篇博客,我們介紹了類加載過程,包括5個階段,分別是“加載”,“驗證”,“准備”,“解析”,“初始化”,如下圖所示:
本篇博客,我們來介紹Java虛擬機的雙親委派模型,在介紹之前,我先拋出一個問題:
我們知道,在JDK源碼中,有各種Java自帶的類,比如java.lang.String,java.util.List等,那么我們自己的項目中,能夠寫一個命名為java.lang.String.java 等JDK源碼中存在的類,並且在項目中使用嗎?
1、類加載器
什么是類加載器?上篇博客我們介紹類加載過程中的第一個階段——加載,作用是“通過一個類的全限定名來獲取描述此類的二進制流”,那么這個加載過程就是由類加載器來完成的。
從Java虛擬機的角度出發,只存在兩種不同的類加載器,一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用 C++ 語言實現,是虛擬機自身的一部分;另一種是所有其它的類加載器,這些類加載器都是由Java語言實現的。但是從Java開發人員的角度來看,類加載器可以細分為如下四種:
①、啟動類加載器(Bootstrap ClassLoader)
負責將存放在 <JAVA_HOME>/lib 目錄中的,或者被-Xbootclasspath 參數所指定的路徑中的,並且是虛擬機按照文件名識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
啟動類加載器無法被Java程序直接引用。
JDK 中的源碼類大都是由啟動類加載器加載,比如前面說的 java.lang.String,java.util.List等,需要注意的是,啟動類 main Class 也是由啟動類加載器加載。
②、擴展類加載器(Extension ClassLoader)
這個類加載器由 sun.misc.Launcher$ExtClassLoader 實現,負責加載<JAVA_HOME>/lib/ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫。
開發者可以直接使用擴展類加載器。
③、應用程序類加載器(Application ClassLoader)
由 sun.misc.Launcher$AppClassLoader 實現。由於這個類加載器是 ClassLoader.getSystemClassLoader() 方法的返回值,所以一般也稱它為系統類加載器。
它負責加載用戶類路徑ClassPath上所指定的類庫,開發者可以直接使用這個類加載器。如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
通常項目中自定義的類,都會放在類路徑下,由應用程序類加載器加載。
④、自定義類加載器(User ClassLoader)
這是由用戶自己定義的類加載器,一般情況下我們不會自定義類加載器,但有些特殊情況,比如JDBC能夠通過連接各種不同的數據庫就是自定義類加載器來實現的,具體用處會在后文詳細介紹。
2、雙親委派模型
回到文章開頭提出的問題,如果有不法分子在你項目中構造了一個java.lang.String類,並在該類中植入了一些不良代碼,但你自己渾然不知,以為使用的String類還是 rt.jar 包下的,那可能會給你系統造成不良的影響。
聰明的Java虛擬機實現者也想到了這個問題,於是,他們引入了 雙親委派模型來解決這個問題。
下面是雙親委派模型的加載流程機制:
總結來說:雙親委派機制就是如果一個類加載器收到了類加載請求,它首先不會自己嘗試去加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有父類加載器反饋到無法完成這個加載請求(它的搜索范圍沒有找到這個類),子加載器才會嘗試自己去加載。
其實,這里叫雙親委派可能有點不妥,因為按道理來講只有父加載器,這里的“雙親”是“parents”的直譯,並不表示漢語中的父母雙親。另外,這里的父加載器也不是繼承的關系。
1 /** 2 * Create by YSOcean 3 */ 4 public class ClassLoadTest { 5 public static void main(String[] args) { 6 ClassLoader classLoader1 = ClassLoadTest.class.getClassLoader(); 7 ClassLoader classLoader2 = classLoader1.getParent(); 8 ClassLoader classLoader3 = classLoader2.getParent(); 9 System.out.println(classLoader1); 10 System.out.println(classLoader2); 11 System.out.println(classLoader3); 12 } 13 }
輸出為:
那么知道了什么是雙親委派機制,雙親委派機制有什么好處呢?
回到上面提出的問題,如果你自定義了一個 java.lang.String類,你會發現這個自定義的String.java可以正常編譯,但是永遠無法被加載運行。因為加載這個類的加載器,會一層一層的往上推,最終由啟動類加載器來加載,而啟動類加載的會是源碼包下的String類,不是你自定義的String類。
3、雙親委派模型實現源碼
可以打開 java.lang.ClassLoader 類,其 loadClass方法如下:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // First, check if the class has already been loaded 6 Class<?> c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) { 11 c = parent.loadClass(name, false); 12 } else { 13 c = findBootstrapClassOrNull(name); 14 } 15 } catch (ClassNotFoundException e) { 16 // ClassNotFoundException thrown if class not found 17 // from the non-null parent class loader 18 } 19 20 if (c == null) { 21 // If still not found, then invoke findClass in order 22 // to find the class. 23 long t1 = System.nanoTime(); 24 c = findClass(name); 25 26 // this is the defining class loader; record the stats 27 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 28 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 29 sun.misc.PerfCounter.getFindClasses().increment(); 30 } 31 } 32 if (resolve) { 33 resolveClass(c); 34 } 35 return c; 36 } 37 }
實現方式很簡單,首先會檢查該類是否已經被加載過了,若加載過了直接返回(默認resolve取false);若沒有被加載,則調用父類加載器的 loadClass方法,若父類加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,則在拋出 ClassNotFoundException 異常后,在調用自己的 findClass 方法進行加載。
4、自定義類加載器
先說說我們為什么要自定義類加載器?
①、加密
我們知道Java字節碼是可以進行反編譯的,在某些安全性高的場景,是不允許這種情況發生的。那么我們可以將編譯后的代碼用某種加密算法進行加密,加密后的文件就不能再用常規的類加載器去加載類了。而我們自己可以自定義類加載器在加載的時候先解密,然后在加載。
②、動態創建
比如很有名的動態代理。
③、從非標准的來源加載代碼
我們不用非要從class文件中獲取定義此類的二進制流,還可以從數據庫,從網絡中,或者從zip包等。
明白了為什么要自定義類加載器,接下來我們再來詳述如何自定義類加載器。
通過第 3 小節的 java.lang.ClassLoader 類的源碼分析,類加載時根據雙親委派模型會先一層層找到父加載器,如果加載失敗,則會調用當前加載器的 findClass() 方法來完成加載。因此我們自定義類加載器,有兩個步驟:
1、繼承 ClassLoader
2、覆寫 findClass() 方法