Java虛擬機器詳解(十一)------雙親委派模型
在上一篇部落格,我們介紹了類載入過程,包括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() 方法
&n