標籤:bootstrap public trap err tac 簡單 強制 私人變數 開發
1 基本資料
每個開發人員對Java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背後就涉及到了java技術體系中的類載入。Java的類載入機制是技術體系中比較核心的部分,雖然和大部分開發人員直接打交道不多,但是對其背後的機理有一定理解有助於排查程式中出現的類載入失敗等技術問題,對理解java虛擬機器的串連模型和java語言的動態性都有很大協助。
2 Java虛擬機器類載入器結構簡述
2.1 JVM三種預定義類型類載入器
我們首先看一下JVM預定義的三種類型類載入器,當一個 JVM啟動的時候,Java預設開始使用如下三種類型類裝入器:
啟動(Bootstrap)類載入器:引導類裝入器是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包載入到記憶體中。由於引導類載入器涉及到虛擬機器本地實現細節,開發人員無法直接擷取到啟動類載入器的引用,所以不允許直接通過引用進行操作。
擴充(Extension)類載入器:擴充類載入器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變數-Djava.ext.dir指定位置中的類庫載入到記憶體中。開發人員可以直接使用標準擴充類載入器。
系統(System)類載入器:系統類別載入器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類別路徑java -classpath或-Djava.class.path變數所指的目錄下的類庫載入到記憶體中。開發人員可以直接使用系統類別載入器。
除了以上列舉的三種類載入器,還有一種比較特殊的類型就是線程上下文類載入器,這個將在後面單獨介紹。
2.2 類載入雙親委派機制介紹和分析
在這裡,需要著重說明的是,JVM在載入類時預設採用的是雙親委派機制。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。關於虛擬機器預設的雙親委派機制,我們可以從系統類別載入器和擴充類載入器為例作簡單分析。
圖一 標準擴充類載入器繼承層次圖
圖二系統類別載入器繼承層次圖
通過圖一和圖二我們可以看出,類載入器均是繼承自java.lang.ClassLoader抽象類別。我們下面我們就看簡要介紹一下java.lang.ClassLoader中幾個最重要的方法:
1 //載入指定名稱(包括包名)的二進位類型,供使用者調用的介面 2 public Class<?> loadClass(String name) throws ClassNotFoundException{ … } 3 4 //載入指定名稱(包括包名)的二進位類型,同時指定是否解析(但是這裡的resolve參數不一定真正能達到解析的效果),供繼承用 5 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … } 6 7 //findClass方法一般被loadClass方法調用去載入指定名稱類,供繼承用 8 protected Class<?> findClass(String name) throws ClassNotFoundException { … } 9 10 //定義類型,一般在findClass方法中讀取到對應位元組碼後調用,可以看出不可繼承 11 //(說明:JVM已經實現了對應的具體功能,解析對應的位元組碼,產生對應的內部資料結構放置到方法區,所以無需覆寫,直接調用就可以了) 12 protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
通過進一步分析標準擴充類載入器(sun.misc.Launcher$ExtClassLoader)和系統類別載入器(sun.misc.Launcher$AppClassLoader)的代碼以及其公用父類(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass(…)方法。既然這樣,我們就可以通過分析java.lang.ClassLoader中的loadClass(String name)方法的代碼就可以分析出虛擬機器預設採用的雙親委派機制到底是什麼模樣:
1 public Class<?> loadClass(String name) throws ClassNotFoundException { 2 return loadClass(name, false); 3 } 4 5 protected synchronized Class<?> loadClass(String name, boolean resolve) 6 throws ClassNotFoundException { 7 8 // 首先判斷該類型是否已經被載入 9 Class c = findLoadedClass(name); 10 if (c == null) { 11 //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入 12 try { 13 if (parent != null) { 14 //如果存在父類載入器,就委派給父類載入器載入 15 c = parent.loadClass(name, false); 16 } else { 17 //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類, 18 //通過調用本地方法native findBootstrapClass0(String name) 19 c = findBootstrapClass0(name); 20 } 21 } catch (ClassNotFoundException e) { 22 // 如果父類載入器和啟動類載入器都不能完成載入任務,才調用自身的載入功能 23 c = findClass(name); 24 } 25 } 26 if (resolve) { 27 resolveClass(c); 28 } 29 return c; 30 }
通過上面的程式碼分析,我們可以對JVM採用的雙親委派類載入機制有了更感性的認識,下面我們就接著分析一下啟動類載入器、標準擴充類載入器和系統類別載入器三者之間的關係。可能大家已經從各種資料上面看到了如下類似的一幅圖片:
圖三 類載入器預設委派關係圖
上面圖片給人的直觀印象是系統類別載入器的父類載入器是標準擴充類載入器,標準擴充類載入器的父類載入器是啟動類載入器,下面我們就用代碼具體測試一下:
1 public class LoaderTest { 2 3 public static void main(String[] args) { 4 try { 5 System.out.println(ClassLoader.getSystemClassLoader()); 6 System.out.println(ClassLoader.getSystemClassLoader().getParent()); 7 System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); 8 } catch (Exception e) { 9 e.printStackTrace(); 10 } 11 } 12 }
說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接擷取到系統類別載入器。
代碼輸出如下:
1 [email protected] 2 [email protected]0dea4e 3 null
通過以上的代碼輸出,我們可以判定系統類別載入器的父載入器是標準擴充類載入器,但是我們試圖擷取標準擴充類載入器的父類載入器時確得到了null,就是說標準擴充類載入器本身強制設定父類載入器為null。我們還是藉助於程式碼分析一下。
我們首先看一下java.lang.ClassLoader抽象類別中預設實現的兩個建構函式:
protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } //預設將父類載入器設定為系統類別載入器,getSystemClassLoader()擷取系統類別載入器 this.parent = getSystemClassLoader(); initialized = true; } protected ClassLoader(ClassLoader parent) { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } //強制設定父類載入器 this.parent = parent; initialized = true; }
聲明為私人變數的同時並沒有對外提供可供衍生類別訪問的public或者protected設定器介面(對應的setter方法),結合前面的測試代碼的輸出,我們可以推斷出:
1. 系統類別載入器(AppClassLoader)調用ClassLoader(ClassLoader parent)建構函式將父類載入器設定為標準擴充類載入器(ExtClassLoader)。(因為如果不強制設定,預設會通過調用getSystemClassLoader()方法擷取並設定成系統類別載入器,這顯然和測試輸出結果不符。)
2. 擴充類載入器(ExtClassLoader)調用ClassLoader(ClassLoader parent)建構函式將父類載入器設定為null。(因為如果不強制設定,預設會通過調用getSystemClassLoader()方法擷取並設定成系統類別載入器,這顯然和測試輸出結果不符。)
現在我們可能會有這樣的疑問:擴充類載入器(ExtClassLoader)的父類載入器被強制設定為null了,那麼擴充類載入器為什麼還能將載入任務委派給啟動類載入器呢?
圖四 標準擴充類載入器和系統類別載入器成員大綱視圖
圖五 擴充類載入器和系統類別載入器公用父類成員大綱視圖
通過圖四和圖五可以看出,標準擴充類載入器和系統類別載入器及其父類(java.NET.URLClassLoader和java.security.SecureClassLoader)都沒有覆寫java.lang.ClassLoader中預設的載入委派規則---loadClass(…)方法。有關java.lang.ClassLoader中預設的載入委派規則前面已經分析過,如果父載入器為null,則會調用本地方法進行啟動類載入嘗試。所以,圖三中,啟動類載入器、標準擴充類載入器和系統類別載入器之間的委派關係事實上是仍就成立的。(在後面的使用者自訂類載入器部分,還會做更深入的分析)。
JAVA類載入器