實習雜記(30):虛擬機類載入機制(3)

  • 2019 年 11 月 14 日
  • 筆記

轉載地址:http://blog.csdn.net/zhoudaxia/article/details/35824249

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. public Class<?> loadClass(String name) throws ClassNotFoundException{ … }
  2. //載入指定名稱(包括包名)的二進位類型,同時指定是否解析(但是這裡的resolve參數不一定真正能達到解析的效果),供繼承用
  3. protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
  4. //findClass方法一般被loadClass方法調用去載入指定名稱類,供繼承用
  5. protected Class<?> findClass(String name) throws ClassNotFoundException { … }
  6. //定義類型,一般在findClass方法中讀取到對應位元組碼後調用,可以看出不可繼承
  7. //(說明:JVM已經實現了對應的具體功能,解析對應的位元組碼,產生對應的內部數據結構放置到方法區,所以無需覆寫,直接調用就可以了)
  8. 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)方法的程式碼就可以分析出虛擬機默認採用的雙親委派機制到底是什麼模樣:

[java] view plain copy

public Class<?> loadClass(String name) throws ClassNotFoundException {
  1. return loadClass(name, false);
  2. }
  3. protected synchronized Class<?> loadClass(String name, boolean resolve)
  4. throws ClassNotFoundException {
  5. // 首先判斷該類型是否已經被載入
  6. Class c = findLoadedClass(name);
  7. if (c == null) {
  8. //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
  9. try {
  10. if (parent != null) {
  11. //如果存在父類載入器,就委派給父類載入器載入
  12. c = parent.loadClass(name, false);
  13. } else {
  14. //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,
  15. //通過調用本地方法native findBootstrapClass0(String name)
  16. c = findBootstrapClass0(name);
  17. }
  18. } catch (ClassNotFoundException e) {
  19. // 如果父類載入器和啟動類載入器都不能完成載入任務,才調用自身的載入功能
  20. c = findClass(name);
  21. }
  22. }
  23. if (resolve) {
  24. resolveClass(c);
  25. }
  26. return c;
  27. }

  通過上面的程式碼分析,我們可以對JVM採用的雙親委派類載入機制有了更感性的認識,下面我們就接著分析一下啟動類載入器、標準擴展類載入器和系統類載入器三者之間的關係。可能大家已經從各種資料上面看到了如下類似的一幅圖片:

圖三 類載入器默認委派關係圖

  上面圖片給人的直觀印象是系統類載入器的父類載入器是標準擴展類載入器,標準擴展類載入器的父類載入器是啟動類載入器,下面我們就用程式碼具體測試一下:

[java] view plain copy

public class LoaderTest {
  1. public static void main(String[] args) {
  2. try {
  3. System.out.println(ClassLoader.getSystemClassLoader());
  4. System.out.println(ClassLoader.getSystemClassLoader().getParent());
  5. System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. }

  說明:通過java.lang.ClassLoader.getSystemClassLoader()可以直接獲取到系統類載入器。   程式碼輸出如下:

[plain] view plain copy

  1. sun.misc.Launcher$AppClassLoader@6d06d69c
  2. sun.misc.Launcher$ExtClassLoader@70dea4e
  3. null

  通過以上的程式碼輸出,我們可以判定系統類載入器的父載入器是標準擴展類載入器,但是我們試圖獲取標準擴展類載入器的父類載入器時確得到了null,就是說標準擴展類載入器本身強制設定父類載入器為null。我們還是藉助於程式碼分析一下。

  我們首先看一下java.lang.ClassLoader抽象類中默認實現的兩個構造函數:

[java] view plain copy

  1. protected ClassLoader() {
  2. SecurityManager security = System.getSecurityManager();
  3. if (security != null) {
  4. security.checkCreateClassLoader();
  5. }
  6. //默認將父類載入器設置為系統類載入器,getSystemClassLoader()獲取系統類載入器
  7. this.parent = getSystemClassLoader();
  8. initialized = true;
  9. }
  10. protected ClassLoader(ClassLoader parent) {
  11. SecurityManager security = System.getSecurityManager();
  12. if (security != null) {
  13. security.checkCreateClassLoader();
  14. }
  15. //強制設置父類載入器
  16. this.parent = parent;
  17. initialized = true;
  18. }

我們再看一下ClassLoader抽象類中parent成員的聲明:

[java] view plain copy

  1. // The parent class loader for delegation
  2. private ClassLoader parent;

  聲明為私有變數的同時並沒有對外提供可供派生類訪問的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,則會調用本地方法進行啟動類載入嘗試。所以,圖三中,啟動類載入器、標準擴展類載入器和系統類載入器之間的委派關係事實上是仍就成立的。(在後面的用戶自定義類載入器部分,還會做更深入的分析)。

2.3 類載入雙親委派示例

  以上已經簡要介紹了虛擬機默認使用的啟動類載入器、標準擴展類載入器和系統類載入器,並以三者為例結合JDK程式碼對JVM默認使用的雙親委派類載入機製做了分析。下面我們就來看一個綜合的例子。首先在IDE中建立一個簡單的java應用工程,然後寫一個簡單的JavaBean如下:

[java] view plain copy

  1. package classloader.test.bean;
  2. public class TestBean {
  3. public TestBean() { }
  4. }

  在現有當前工程中另外建立一測試類(ClassLoaderTest.java)內容如下:

  測試一:

[java] view plain copy

  1. package classloader.test.bean;
  2. public class ClassLoaderTest {
  3. public static void main(String[] args) {
  4. try {
  5. //查看當前系統類路徑中包含的路徑條目
  6. System.out.println(System.getProperty("java.class.path"));
  7. //調用載入當前類的類載入器(這裡即為系統類載入器)載入TestBean
  8. Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
  9. //查看被載入的TestBean類型是被那個類載入器載入的
  10. System.out.println(typeLoaded.getClassLoader());
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

對應的輸出如下:

[plain] view plain copy

  1. C:UsersJackZhouDocumentsNetBeansProjectsClassLoaderTestbuildclasses
  2. sun.misc.Launcher$AppClassLoader@73d16e93

說明:當前類路徑默認的含有的一個條目就是工程的輸出目錄。 測試二:

  將當前工程輸出目錄下的TestBean.class打包進test.jar剪貼到<Java_Runtime_Home>/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待載入類型的class文件)。再運行測試一測試程式碼,結果如下:

[plain] view plain copy

  1. C:UsersJackZhouDocumentsNetBeansProjectsClassLoaderTestbuildclasses
  2. sun.misc.Launcher$ExtClassLoader@15db9742

  對比測試一和測試二,我們明顯可以驗證前面說的雙親委派機制,系統類載入器在接到載入classloader.test.bean.TestBean類型的請求時,首先將請求委派給父類載入器(標準擴展類載入器),標準擴展類載入器搶先完成了載入請求。   測試三:   將test.jar拷貝一份到<Java_Runtime_Home>/lib下,運行測試程式碼,輸出如下:

[plain] view plain copy

  1. C:UsersJackZhouDocumentsNetBeansProjectsClassLoaderTestbuildclasses
  2. sun.misc.Launcher$ExtClassLoader@15db9742

  測試三和測試二輸出結果一致。那就是說,放置到<Java_Runtime_Home>/lib目錄下的TestBean對應的class位元組碼並沒有被載入,這其實和前面講的雙親委派機制並不矛盾。虛擬機出於安全等因素考慮,不會載入<Java_Runtime_Home>/lib存在的陌生類,開發者通過將要載入的非JDK自身的類放置到此目錄下期待啟動類載入器載入是不可能的。做個進一步驗證,刪除<Java_Runtime_Home>/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然後再運行測試程式碼,則將會有ClassNotFoundException異常拋出。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點運行測試三進行調試,會發現findBootstrapClass0()會拋出異常,然後在下面的findClass方法中被載入,當前運行的類載入器正是擴展類載入器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變數視圖查看驗證。