ClassLoader 中除了“雙親委派”,這些細節更應該瞭解

提到 ClassLoader,最先想到的一定是“雙親委派”了,載入類時優先使用父類載入器(parent classloader),不過除了這個委託模型之外,還有很多細節值得研究

載入時機

除了顯示呼叫 ClassLoader。loadClass 進行載入 Class 之外,JVM 在下面的5種場景下,也會執行載入 Class 的操作(由 JVM 呼叫 ClassLoader。loadClassInternal)

使用 new 關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

使用 java。lang。reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含 main()方法的那個類),虛擬機器會先初始化這個主類。

當使用 JDK 1。7 的動態語言支援時,如果一個 java。lang。invoke。MethodHandle 例項最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

以上幾種載入時機,統稱為主動引用的方式;除此之外,其他引用類的方式都不會被觸發 Class 的載入

比如下面這種情況,就不屬於主動引用,不會進行類的載入

public class NotInitialization { public static void main(String[] args) { //根據場景1,讀取的是常量,不會造成類的初始化 System。out。println(ConstClass。HELLOWORLD); }}class ConstClass{ static final String HELLOWORLD = “hello world”; static { System。out。println(“ConstClass init!”); }}

Class。forName

透過 Class。forName 的形式,可以進行進行類的載入,不過這裡使用的是呼叫者(caller)的類載入器,也就是發起Class。forName 方法呼叫的類的類載入器

public static Class<?> forName(String className) throws ClassNotFoundException { //獲取caller Class Class<?> caller = Reflection。getCallerClass(); return forName0(className, true, ClassLoader。getClassLoader(caller), caller);}

類載入器 的“傳遞性”

首先說一下兩個概念,

定義載入器(defining loader)

初始載入器(initiating loader)

現有一個 ClassLoader A,如果透過A直接定義(而不是在A內委託 parent classLoader 載入)了一個 Class X,那麼稱 ClassLoader A 是 Class X 的

定義載入器

,也稱 ClassLoader A定義了 Class X

當一個 ClassLoader 委託 parent classLoader 進行載入某類(loadClass),那麼此時 loadClass 的 classLoader 和實際上 defineClass 的 parent classLoader 其實並不是同一個

現有一個 ClassLoader B,透過 ClassLoader B 的 loadClass 方法載入 Class Y,無論ClassLoader B 是直接 define了Class Y,還是委託 parent classLoader 去 define 了Class Y,那麼 ClassLoader B 都是 Class Y的

初始類載入器

只不過如果 Class Y 是由 ClassLoader B 直接載入的,那麼 Class Y的

定義載入器

初始載入器

都是ClassLoader B;如果是委託父類 parent classLoader C載入的,那麼

定義載入器

就是ClassLoader C,而

初始載入器

就是 ClassLoader B

如下圖所示,對於被載入的Class X來說,ClassLoader A就是

定義載入器

,而實際上 ClassLoader A是委託了 ClassLoader B 去完成 define 的,所以 ClassLoader B 是

定義載入器

ClassLoader 中除了“雙親委派”,這些細節更應該瞭解

使用 Class。forName 這種形式載入的 Class,實際上也是使用呼叫者(caller)的

定義載入器

這段雖然像繞口令一樣……但這也是 ClassLoader 的關鍵,ClassLoader 的傳遞特性非常重要

對於上面提到的

“主動引用”

方式的載入Class,在載入機制上有一些細節需要注意:

JVM在解釋 Class 時實際上 是

“惰性載入”

的,在解釋執行的行遇到引用後才會解析引用的 Class;比如在方法體中呼叫了某類,只有在執行到這一行時才會進行載入

對於同一個 ClassLoader 例項來說,在當前 Class 中沒有載入過的 Class,會使用

發起引用類的定義載入器(而不是 system ClassLoader)

進行載入

如果一個 ClassLoader 中已經載入過某個 Class,那麼就不會再載入(連 loadClass 都不會呼叫);比如 Class A裡引用了 Class X 和 Class B,Class B 裡也引用了 Class X,那麼在執行 Class B 時,不會再發生 loadClass(X) 的操作

根據上面幾個特點,自定義 ClassLoader 就比較簡單了,比如 Spring Boot 提供的可執行Jar(Executable Jar)的 ClassLoader 實現中:

只需要建立一個負責載入 jar 包內 jar 包的 ClassLoader(org。springframework。boot。loader。JarLauncher),然後入口類中透過該 ClassLoader 去載入我們程式碼中的 Main-Class 即可,這樣就可以做到載入 jar 包中的 jar 包了:

public void run() throws Exception { //透過前面設定的“負載載入jar包內jar包的ClassLoader”,去載入我們程式中的main類 Class<?> mainClass = Thread。currentThread()。getContextClassLoader() 。loadClass(this。mainClassName); Method mainMethod = mainClass。getDeclaredMethod(“main”, String[]。class); mainMethod。invoke(null, new Object[] { this。args });}

作者:空無

連結:https://juejin。cn/post/6950834844741926919