一文弄懂Java中類加載器的關係

學習總結於-《深入瞭解JAVA虛擬機》-周自明

在這裏插入圖片描述在這裏插入圖片描述

Java虛擬機的類加載機制

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是Java虛擬機的類加載機制

Java類具體是如何被加載的,請參照文章:一文淺析Java中類加載過程

在瞭解Java虛擬機是如何進行類加載過程之後,就進行學習今天的主角兒,什麼是類加載器?
在類加載過程中的第一個動作-“加載”這個動作時,它是通過一個類的全限定名來獲取定義此類的二進制流。實現這個動作的代碼模塊叫做“類加載器”!

站在不同的角度看類加載器

站在Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),另一種就是其他類加載器。

  • 啓動類加載器:這個類加載器是使用C++語言實現,是虛擬機自身的一部分。
  • 其他類加載器:這些加載器都是由Java語言實現,獨立於虛擬機外部,並且全部都繼承於抽象類java.lang.ClassLoader。

站在Java開發人員角度來講, 類加載器還可以分得更細緻一些,可以分爲下面3種類加載器,分別爲:啓動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)和 應用程序類加載器(Application ClassLoader)

  • 啓動類加載器:前面介紹過,這個類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者是被 -Xbootclasspath 參數所指定的路徑中的類庫加載到虛擬機內存中
  • 擴展類加載器:這個類加載器負責加載<JAVA_HOME>\lib\ext目錄中,或者被
    java.ext.dirs系統變量所指定的路徑中的類庫。
  • 應用程序類加載器:這個類加載器是 ClassLoader 中的 getSystemLoader()
    方法的返回值,所以也稱爲-系統類加載器。它負責加載 ClassPath 上所指定的類庫。

查看類加載器加載的內容

爲了印證我們上面的結論,下面就是用代碼看看不同的類加載器加載的內容是什麼。

查看啓動類加載器

public static void main(String[] args) {
    System.out.println(System.getProperty("sun.boot.class.path"));
}

輸出結果:

D:\Java\jdk\jre\lib\resources.jar;
D:\Java\jdk\jre\lib\rt.jar;
D:\Java\jdk\jre\lib\sunrsasign.jar;
D:\Java\jdk\jre\lib\jsse.jar;
D:\Java\jdk\jre\lib\jce.jar;
D:\Java\jdk\jre\lib\charsets.jar;
D:\Java\jdk\jre\lib\jfr.jar;
D:\Java\jdk\jre\classes

查看擴展類加載器

public static void main(String[] args) {
    System.out.println(System.getProperty("java.ext.dirs"));
}

輸出結果:

D:\Java\jdk\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext

查看應用程序類加載器

public static void main(String[] args) {
    System.out.println(System.getProperty("java.class.path"));
}

這個加載器會去加載 ClassPath 下面的內容、會去加載 Jdk安裝目錄下 jre\lib\ 下面的內容。如果用了 Maven ,也會去加載 Maven 本地倉庫下面的一些所需內容。

一般情況下,如果我們沒有自己定義自己的類加載器的話,應用程序類加載器(Application ClassLoader)就是默認的類加載器。

類加載器之間的關係

這些類加載器之間的關係下面這樣的:
在這裏插入圖片描述
上面兩張圖展示了 應用程序類加載器(AppClassLoader)和 擴展類加載器(ExtClassLoader)的類關係圖,他們都繼承於 URLClassLoader。

除了頂層的啓動類加載器(Bootstrap ClassLoader)之外,其餘的類加載器都應當有自己的父加載器。這裏加載器之間的父子關係不是使用繼承的關係來實現的,所以這裏 URLClassLoader 並不是 AppClassLoader 和 ExtClassLoader 的父加載器。那麼他們的父加載器到底是誰呢?

接下來從代碼裏面來找找答案。

印證類加載器之間的關係

首先我們先隨便創建一個 Java 文件,然後寫一個 main 方法,在裏面使用 getClassLoader() 方法來查看加載關係,如下面代碼所示:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(ClassLoaderTest.class.getClassLoader());
    }
}

結果輸出:

sun.misc.Launcher$AppClassLoader@18b4aac2

從結果可以看出 ClassLoaderTest.class 文件是由 AppClassLoader 也就是應用程序類加載器加載的。

這個時候長得美麗/帥氣並且聰明的同學可能就有疑問了,說:ClassLoaderTest 這個類是我們自己創建的,它是 AppClassLoader 加載的可以理解,那麼如果是我們經常使用的 int、String、boolean等等這些基礎類是被誰加載的呢?

我們不妨參照上面的例子,再來試試:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(int.class.getClassLoader());
        System.out.println(String.class.getClassLoader());
        System.out.println(boolean.class.getClassLoader());
    }
}

結果輸出:

null
null
null

在這裏插入圖片描述
Why???結果是null,意思是 int.class 和String.class 這類基礎類沒有類加載器加載?當然不是!我們用到的每一個類都必須有類加載器去加載它,然後我們才能是使用。

我先公佈正確答案: int 和 String 這類基礎類是由 Bootstrap ClassLoader 加載的,最直觀的解釋就是這些類是在 java.lang 包裏,而這個包是在 rt.jar 裏面,通過最上面的解釋說 rt.jar 是由 Bootstrap ClassLoader 加載的。這樣說可能站不住腳!我會在後面詳細解釋填坑,但是我們要弄明白後面的內容,我們首先得知道一個前提:每個類加載器都有一個父加載器

每個類加載器都有一個父加載器

每個類加載器都有一個父加載器,比如 ClassLoaderTest.class 是由 AppClassLoader 加載的,那麼 AppClassLoader 應該也有一個父加載器,那麼如何得知它的父加載器是誰呢?很簡單,通過 getParent 方法,就像這樣:

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 首先獲取 appClassLoader
        ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
        // 再獲取它的父加載器
        ClassLoader parentLoader = appClassLoader.getParent();
        System.out.println(parentLoader);
    }
}

結果輸出:

sun.misc.Launcher$ExtClassLoader@4617c264

從結果可以看出 AppClassLoader 的父加載器是 ExtClassLoader 。那麼 ExtClassLoader 的父加載器又是誰呢?

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 首先獲取 appClassLoader
        ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
        // 再獲取 extClassLoader
        ClassLoader extClassLoader = appClassLoader.getParent();
        // 最後獲取 extClassLoader 的父加載器
        ClassLoader parentLoader = extClassLoader.getParent();
        System.out.println(parentLoader);
    }
}

結果輸出:

null

哦豁!!又是一個 null。
這表明 ExtClassLoader 沒有父加載器???那麼,爲什麼標題又是每一個加載器都有一個父加載器呢?這不矛盾嗎?爲了解釋這一點,我們還需要繼續往下看

父加載器不是父類

注意: 下面進入查看源碼部分,下面這一段源碼只是爲了證明解釋爲什麼 AppClassLoader 的父加載器是 ExtClassLoader,而 ExtClassLoader 的父加載器是 null這個現象。 答案都在源碼裏。爲了源碼看起來不枯燥,我將把最重要的最能夠說明問題的那段代碼展示出來,並且添加註釋,希望在學習到這裏的時候,大家跟着文章列舉出的類,自己點到源碼裏面去看一看。但是不要看太多了,容易迷失,適可而止。
在這裏插入圖片描述

先來看看 AppClassLoader 類繼承關係:

static class AppClassLoader extends URLClassLoader{...}

再來看看 ExtClassLoader 類繼承關係:

static class ExtClassLoader extends URLClassLoader{...}

可以發現 AppClassLoader 和 ExtClassLoader 都是繼承自 URLClassLoader,而且他們都是 URLClassLoader 的內部類。在上面一小節代碼中,爲什麼調用 AppClassLoader 的 getParent() 代碼會得到 ExtClassLoader 的實例呢?

接下來帶着問題去看看 sun.misc.Launcher.class 的部分關鍵源碼(我將它稱之爲:代碼片段-1):

// Launcher 類的無參構造器
// 在這個構造器裏面創建了 AppClassLoader 和 ExtClassLoader 的實例
public Launcher() {
		// 持有 ExtClassLoader 的引用,爲什麼寫作是 “Launcher.” 呢?因爲 ExtClassLoader 和 AppClassLoader 都是 URLClassLoader 的內部類。
	    Launcher.ExtClassLoader var1;
	    try {
			// 創建 ExtClassLoader 實例,有興趣的同學可以點進去看一下,裏面是實現了一個單例。
			// 另外請注意:這裏沒有傳參數,注意這個細節,下面會解釋!
	        var1 = Launcher.ExtClassLoader.getExtClassLoader();
	    } catch (IOException var10) {
	        throw new InternalError("Could not create extension class loader", var10);
	    }
	
	    try {
			// 創建 AppClassLoader 實例。將 ExtClassLoader 對象作爲參數傳進去,注意這個細節,下面會解釋!
	        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
	    } catch (IOException var9) {
	        throw new InternalError("Could not create application class loader", var9);
	    }
	
	    ...... // 省略此方法剩餘代碼

}

我們首先點擊 getAppClassLoader(var1) 方法,進入 AppClassLoader 的源碼。

接下來看看 AppClassLoader 部分源碼(我將它稱之爲:代碼片段-2),看看上面將 ExtClassLoader 對象作爲參數傳進來做了什麼:

static class AppClassLoader extends URLClassLoader {
       final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

       public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
           final String var1 = System.getProperty("java.class.path");
           final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
           return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
               public Launcher.AppClassLoader run() {
                   URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                   // 創建 AppClassLoader 時,傳進來的參數(ExtClassLoader)-var0 沒有進行任何加工,
                   // 繼續往 AppClassLoader 的有參構造方法裏傳。
                   return new Launcher.AppClassLoader(var1x, var0);
               }
           });
       }
       // AppClassLoader 有參構造器,傳進來的參數(ExtClassLoader)-var2。
       // 繼續傳往父類構造器,也就是 URLClassLoader 類的構造器
       AppClassLoader(URL[] var1, ClassLoader var2) {
           super(var1, var2, Launcher.factory);
           this.ucp.initLookupCache(this);
       }
	
	...... // 省略此方法剩餘代碼
	
}

接下來看看 java.net.URLClassLoader 的構造器裏面做了什麼(我將它稱之爲:代碼片段-3):

// 注意參數列表,第二參數就是上面 AppClassLoader 有參構造器調用 super 方法傳的第二參數也就是 ExtClassLoader 對象。
// 而且這裏的名字變成了 parent,你明白什麼意思了嗎?
// 在加上對註釋的解釋:@param parent the parent class loader for delegation 說的是 @param parent 委託的父類加載器
public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
	// 雖然這裏繼續往父類傳的,但是代碼看到這裏就可以了
	// 有興趣的同學可以去看一下。到這裏已經找到我們所需要的答案了。適可而止,避免迷失!
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    acc = AccessController.getContext();
    ucp = new URLClassPath(urls, factory, acc);
}

從 URLClassLoader 的構造器裏面我們可以得出結論:AppClassLoader 的父加載器是 ExtClassLoader!因爲在創建 AppClassLoader 就把 ExtClassLoader 設置成了它的父加載器

到此!AppClassLoader 的父加載器爲什麼是是 ExtClassLoader已經解釋清楚了,沒有弄明白的同學建議再反覆看看多琢磨琢磨,接下來解釋爲什麼 ExtClassLoader 的父加載器爲什麼是 null !

我們再回到最初的起點,代碼片段-1的內容,我將它 copy 一份下來,免得上下去看:

 // Launcher 類的無參構造器
public Launcher() {
	   // 持有 ExtClassLoader 的引用,爲什麼寫作是 “Launcher.” 呢?因爲 ExtClassLoader 和 AppClassLoader 都是 URLClassLoader 的內部類。
       Launcher.ExtClassLoader var1;
       try {
	      // 創建 ExtClassLoader 實例,有興趣的同學可以點進去看一下,裏面是實現了一個單例。
	      // 另外請注意:這裏沒有傳參數,注意這個細節,下面會解釋!
           var1 = Launcher.ExtClassLoader.getExtClassLoader();
       } catch (IOException var10) {
           throw new InternalError("Could not create extension class loader", var10);
       }

       try {
		   // 創建 AppClassLoader 實例。將 ExtClassLoader 對象作爲參數傳進去,注意這個細節,下面會解釋!
           this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
       } catch (IOException var9) {
           throw new InternalError("Could not create application class loader", var9);
       }

       ...... // 省略此方法剩餘代碼

}

我之所以選擇先解釋 AppClassLoader 的源碼內容,其實是爲了解釋 ExtClassLoader 的源碼做鋪墊。

上面說到 getExtClassLoader() 方法其實是構建了一個單例,我們看看是怎麼構建的。點擊 getExtClassLoader() 方法,進入 ExtClassLoader 部分源碼。

注意:下面這塊代碼稍微有點多,但是不用擔心,我們可以將它分爲三個部分來看

static class ExtClassLoader extends URLClassLoader {
       private static volatile Launcher.ExtClassLoader instance;
	
	  // 第一部分:這裏就是 getExtClassLoader() 方法構建單例的地方了。
       public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
           if (instance == null) {
               Class var0 = Launcher.ExtClassLoader.class;
               synchronized(Launcher.ExtClassLoader.class) {
                   if (instance == null) {
                    // 最主要的地方在這裏,這裏在創建 instance 的時候,調用了 createExtClassLoader() 方法,接下來進入第二部分。
					instance = createExtClassLoader();
                   }
               }
           }

           return instance;
       }

	   // 第二部分:這裏就是 createExtClassLoader() 方法了。
       private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
           try {
               return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                   public Launcher.ExtClassLoader run() throws IOException {
                       // 小收穫:這裏獲取 jdk 擴展目錄下面的內容,也就是 System.getProperty("java.ext.dirs") 下面的內容。
                       // 這個方法我在文章開頭展示各個類加載器加載了什麼東西的時候講過,忘記了的可以回去看看。這裏不是重點哈。
					File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                       int var2 = var1.length;

                       for(int var3 = 0; var3 < var2; ++var3) {
                           MetaIndex.registerDirectory(var1[var3]);
                       }
					  // 主要看這裏,這個方法最後會調用到這裏來,調用了 ExtClassLoader 的有參構造器,下面進入第三部分
                       return new Launcher.ExtClassLoader(var1);
                   }
               });
           } catch (PrivilegedActionException var1) {
               throw (IOException)var1.getException();
           }
       }

	   // 第三部分:ExtClassLoader 的有參構造器。
       public ExtClassLoader(File[] var1) throws IOException {
		   // 注意這裏,它調用了父類的構造器,也就是 也就是 URLClassLoader 類的構造。
		   // 如果你夠細心的話,會發現 appClassLoader 也是調用了這個父類構造器。
           super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
           SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
       }
}	

接下來再次看看 java.net.URLClassLoader 的構造器,其實那塊代碼就是代碼片段-3,我將它Copy過來,對註釋稍作修改:

// 注意參數列表,第二參數就是上面 ExtClassLoader 有參構造器調用 super 方法傳的第二參數也就是 null。
// 而且這裏的名字變成了 parent,你明白什麼意思了嗎?
// 在加上對註釋的解釋:@param parent the parent class loader for delegation 說的是 @param parent 委託的父類加載器
public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
	// 雖然這裏繼續往父類傳的,但是代碼看到這裏就可以了。
	// 有興趣的同學可以去看一下。到這裏已經找到我們所需要的答案了。適可而止,避免迷失!
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    acc = AccessController.getContext();
    ucp = new URLClassPath(urls, factory, acc);
}

我們再次從 URLClassLoader 的構造器裏面得出結論:ExtClassLoader 的父加載器就是 null!因爲在創建 ExtClassLoader 就把 null 設置成了它的父加載器

通過上面這一段源碼算是證明解釋爲什麼 AppClassLoader 的父加載器是 ExtClassLoader,而 ExtClassLoader 的父加載器是 null這個現象!!!!這算是填了一個坑了。

還有一個坑沒填:爲什麼 int 和 String 這類基礎類通過 getClassLoader() 方法得到的結果是 null??

好累~休息一會兒
在這裏插入圖片描述

啓動類加載器 Bootstrap ClassLoader

上文說到,Bootstrap ClassLoader 是使用C++來實現的(在HotSpot中),它是虛擬機自身的一部分,所以它的實例也就無法被我們獲取到。

int、String這些常用類是放在 java.lang 包中的,java.lang 又在 rt.jar 包中,而 rt.jar 包是被 Bootstrap ClassLoader 加載器所加載的。

所以得出結論:int 和 String 這類基礎類通過 getClassLoader() 方法得到的結果是 null,實屬正常情況。因爲它們的類加載器是 Bootstrap ClassLoader,而我們無法得到這個類加載器的實例。

上面通過好幾個代碼片段,解釋了 appClassLoader 的父加載器是 extClassLoader, 但是並未解釋爲什麼 extClassLoader 的父加載器就是 Bootstrap ClassLoader。 接下來帶着疑問再次來捋一捋它們之間的關係。

appClassLoader、extClassLoader 和 Bootstrap ClassLoader 的關係

我們在看上面列舉出來源碼片段的時候可以注意一下 appClassLoader、extClassLoader 和 URLClassLoader 是屬於 java.net 包裏面的,Launcher 是屬於 sun.misc 包裏面的。 最重要的是它們都是在 rt.jar 包裏面。
rt.jar 包已經反覆多次被提到了,它是被 Bootstrap ClassLoader 加載器加載的。

我們上面看到的所有源碼內容都是被 Bootstrap ClassLoader 加載器加載的,Launcher 類在初始化的時候又將 extClassLoader 設置爲 appClassLoader 的父加載器。現在這三個加載器的關係很微妙了。

extClassLoader 和 appClassLoader 父子關係,但是它倆又是被 bootstrap ClassLoader 加載的,所以自然而然 bootstrap ClassLoader 就成了 extClassLoader 的父加載器。不知道有沒有理解這個道理。
在這裏插入圖片描述

類加載器就介紹到這裏,先消化了這塊內容後,後面再繼續講 類加載器的雙親委派模型 和 如何自定義類加載。

總結

  • 加載器有仨(自定義類加載器除外):啓動類加載器 Bootstrap ClassLoader (爺爺)、擴展類加載器 Extension
    ClassLoader(爸爸)和 應用程序類加載器 Application ClassLoader(兒子),自定義加載器就是孫子啦。

  • 各個加載器掃描的範圍:
    啓動類加載器 Bootstrap ClassLoader:加載的範圍是一些核心類庫,將存放在 <JAVA_HOME>\lib 目錄中的,或者是被 -Xbootclasspath 參數所指定的路徑中的類庫加載到虛擬機內存中

    擴展類加載器 Extension ClassLoader:加載的範圍是一些擴展類庫,負責加載<JAVA_HOME>\lib\ext目錄中,或者被 java.ext.dirs系統變量所指定的路徑中的類庫。

    應用程序類加載器 Application ClassLoader:負責加載 ClassPath 上所指定的類庫


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章