深入理解JVM虛擬機器:(六)虛擬機器類載入機制(下)

前言

上一章中深入理解JVM虛擬機器:(五)虛擬機器類載入機制(上),我們介紹了虛擬機器的類載入機制,這一章,我們繼續聊類載入機制。

解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,符號引用在前一章講解Class檔案格式的時候已經出現過了多次,在Class檔案中它以CONSTANT_Class_info、CONSTAN)Fieldref_info、CONSTANT_Methodref_info等型別的常量出現,那解析階段中所說的直接引用與符號引用又有什麼關聯呢?

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是他們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
  • 直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經存在在記憶體中。

虛擬機器規範中並未規定解析階段發生的具體時間,只要求了在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic這16個用於操作符號引用的位元組碼指令之前,先對他們所使用的符號引用進行解析。所以虛擬機器實現可以根據需要來判斷到底是在類載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才去解析它。
對同一個符號引用進行多次解析請求是很常見的事情,除invokedynamic指令以外,虛擬機器實現可以對第一次解析的結果進行快取(在執行時常量池中記錄直接引用,並把常量標識為已解析狀態)從而避免解析動作重複進行。無論是否真正執行了多次解析動作,虛擬機器需要保證的是在同一個實體中,如果一個符號引用之前已經被成功解析過,那麼後續的引用解析請求就應當一直成功;同樣的,如果第一次解析失敗了,那麼其他指令對這個符號的解析請求也應該收到相同的異常。
對於invokedynamic指令,上面的規則則不成立。當碰到某個前面已經由invokedynamic指令觸發過解析的符號引用時,並不意味著這個解析結果對於其他invokedynamic指令也同樣生效。因為invokedynamic指令的目的本來就是用於動態語言支援,它所對應的引用稱為“動態呼叫點限定符”,這裡的“動態”的含義就是必須等到程式實際執行到這條指令的時候,解析動作才能進行。相對的,其餘可出發解析的指令都是“靜態”的,可以在剛剛完成載入階段,還沒開始執行程式碼時就進行解析。
解析動作主要針對類或者介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行,分別對應於常量池的CONSTAN_Class_info、CONSTAN_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7種常量型別。下面將講解前面4種的解析過程。

1、類或介面的解析

假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或者介面C的直接引用,

public class D {
public static N n = new C();
}

那虛擬機器完成整個解析的過程需要下面3個步驟:
1. 如果C不是一個陣列型別,那麼虛擬機器將會把代表N的全限定名傳遞給D的類載入器去家在這個類C。在載入過程中沒有雨後設資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的載入動作,例如家在這類的父類或者實現的介面。一旦這個載入過程出現了任何異常,解析過程就宣告失敗。

public class D {
public static Integer [] n = new Integer[3];
}
  1. 如果C是一個陣列型別,並且陣列的元素型別為物件,也就是N的描述符會是一個類似“[Ljava/lang/Integer”的形式,那將會按照第1點的規則載入陣列元素型別。如果N的描述符如前面所假設的形式,需要載入的元素型別就是“java.lang.Integer”,接著由虛擬機器生成一個代表此陣列維度和元素的資料物件。
  2. 如果上面的步驟沒有出現任何的異常,那麼C在虛擬機器中實際上已經成為一個有效的類或者介面了,但在解析完成之前還要進行符號引用驗證,確認D是否具備對C的訪問許可權。如果發現不具備訪問許可權,將丟擲java.lang.IllegalAccessError異常。

2、欄位解析

要解析一個未被解析過的欄位符號引用,首先將會對欄位表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是欄位所屬的類或者介面的符號引用。如果在解析這個類或者介面符號引用的過程中出現了任何的異常,都會導致欄位符號引用解析的失敗。
如果解析成功完成,那將這個欄位所屬的類或者介面用C表示,虛擬機器規範要求按照如下步驟對C進行後續欄位的搜尋。
1. 如果C本身就包含了簡單名稱和欄位名描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
2. 否則,如果在C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
3. 否則,如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
4. 否則,查詢失敗,丟擲java.lang.NoSuchFieldError異常。

如果查詢過程成功返回了引用,將會對這個欄位進行許可權驗證,如果發現不具備欄位的訪問許可權,將丟擲java.lang.IllegalAccessError異常。

在實際應用中,虛擬機器的編譯器實現可能會比上述規範要求更加的嚴格一些,如果有一個同名欄位同時出現在C的介面和父類中,或者同時在自己或父類的多個介面中出現,那編譯器將可能拒絕編譯。下面的程式碼示例中,如果註釋了Sub類中的“public static int A = 4;”,介面與父類同時存在欄位A,那編譯器將提示”The field Sub.A is ambiguous”,並且拒絕編譯這段程式碼。

public class TestClass {
interface Interface0 {
int A = 0;
}
interface Interface1 extends Interface0 {
int A = 1;
}
interface Interface2 {
int A = 2;
}
static class Parent implements Interface1 {
public static int A = 3;
}
static class Sub extends Parent implements Interface2 {
public static int A = 4;
}
public static void main(String[] args) {
System.out.println(Sub.A);
}
}

3、類方法解析

類方法解析的第一個步驟與欄位解析一樣,也需要先解析出類方法表的class_index項中索引的方法所屬的類或者介面的符號引用,如果解析成功,我們依然用C表示這個類,接下來虛擬機器將會按照如下步驟進行後續的類方法搜尋。

  1. 類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是個介面,那就直接丟擲ava.lang.IncompatibleClassChangeError異常。
  2. 如果通過了第一步,在類C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  3. 否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  4. 否則,在類C實現的介面列表及它們的父類介面中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,這時查詢結束,丟擲java.lang.AbstractMethodError異常。
  5. 否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError。

最後,如果查詢過程成功返回了直接引用,將會對這個方法進行許可權驗證,如果發現不具備對此方法的訪問許可權,將丟擲java.lang.IllegalAccessError異常。

4、介面方法解析

介面方法也需要先解析出介面方法表的class_index項中索引的方法所屬的類或介面的符號引用,如果解析成功,依然用C表示這個介面,接下來虛擬機器將會按照如下步驟進行後續的介面方法搜尋。
1. 與類方法解析不同,如果在介面方法表中發現class_index中的索引C是個類而不是介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
2. 否則,在介面C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
3. 否則,在介面C的父介面中遞迴查詢,直到java.lang.Object類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
4. 否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError異常。

由於介面中的所有方法預設都是public的,所以不存在訪問許可權的問題,因此介面方法的符號解析應當不會丟擲java.lang.IllegalAccessError異常。

初始化

類初始化階段是類載入過程的最後一步,前面的類載入過程中,除了在載入階段使用者應用程式可以通過自定義載入類載入器參與外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,財政在開始執行類中定義的Java程式程式碼。

在準備階段,變數已經賦過一次系統要求的初始化值,而在初始化階段,則根據程式設計師通過程式制定的主觀計劃去初始化類變數和其他資源,或者可以從另外一個角度去表達:初始化階段是執行類構造器()方法執行過程中一些可能會影響程式執行行為的特點和細節,這部分相對更貼近於普通的程式開發人員。

  • ()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,當時不能訪問。
public class Test { 
static {
i = 0;                  //給變數賦值可以正常編譯通過
System.out.print(i);    //這句編譯器會提示“非法向前引用”
}
static int i = 1;
}
  • ()方法與類的構造器不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的()方法執行之前,父類的()方法已經執行完畢。因此在虛擬機器中第一個被執行的()方法的類肯定是java.lang.Object。
  • 由於父類的()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作,如下程式碼中,欄位B的值將會是2而不是1。
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
  • ()方法對於類或者介面來說並不是必需的,如果一類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯可以不為這個類生成()方法。
  • 介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成()方法。但是介面與類不同的是,執行介面的()方法不需要先執行父介面的()方法。只有當膚疾克中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始時也一樣不會執行介面的()方法。
  • 虛擬機器會保證一個類的()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的()方法,其他執行緒都需要阻塞等待,知道活動執行緒執行()方法完畢。如果在一個類的()方法中有耗時很長的操作,就可能造成多個程序阻塞,在實際應用中這種阻塞往往是很隱蔽的。

類載入器

虛擬機器設計團隊把類載入階段中的“通過一個雷的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java機外部來實現,以便讓應用程式自己決定如何獲取所需的類。實現這個動作的程式碼模組稱為“類載入器”。
類載入器可以說是Java語言的一項創新,也是Java語言流行的重要原因之一,它最初是為了滿足Java Applet的需求而開發出來的。雖然目前Java Applet技術基本上已經“死掉”,但類載入器卻在類層次劃分、OSGi、熱部署、程式碼加密等領域大放光彩,成為了Java技術體系中的一塊重要的基石,可謂是失之桑榆,收之東偶。

類與類載入器

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠遠不限於類載入階段,對於任意一個類,都需要由載入它的類載入器和這個累本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話可以表達的更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器的前提下才有意義,否則,基石這兩個類來源自同一個Class檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等。
這裡所指的“相等”,包括代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做物件所屬關係判定等情況。如果沒有注意到類載入器的影響,在某些情況下可能會產生具有迷惑性的結果,下面程式碼演示了不同的類載入器對instanceof關鍵字運算的結果的影響。

package com.xuangy.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".")   1)   ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object object = myClassLoader.loadClass("com.xuangy.classloader.ClassLoaderTest").newInstance();
System.out.println(object.getClass());
System.out.println(object instanceof com.xuangy.classloader.ClassLoaderTest);
}
}
執行結果:
class com.xuangy.classloader.ClassLoaderTest
false

上面的程式碼中構造了一個簡單的類載入器,儘管很簡單,但是對於這個演示來說還是夠用了。它可以載入與自己在同一路徑一下的Class檔案。我們使用這個類載入器去載入了一個名為“com.xuangy.classloader.ClassLoaderTest”的類,並例項化了這個類的物件。兩行輸出結果中,從第一句可以看出,這個物件確實是com.xuangy.classloader.ClassLoaderTest例項化出來的物件,但從第二句可以發現,這個物件與類com.xuangy.classloader.ClassLoaderTest做所屬型別檢查的時候卻返回了false,這是因為虛擬機器中存在了兩個ClassLoaderTest類,一個是由系統應用程式類載入器載入的,另外一個是由我們自定義的類載入器載入的,雖然都來自與同一個Class檔案,但仍然是兩個獨立的類,做物件所屬型別檢查時結果自然是false。

雙親委派模型

從Java虛擬機器的角度來講,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),這個類載入器使用C 語言實現,是虛擬機器自身的一部分;另一種就是所有其他的類載入器,這些類載入器都由Java語言實現,獨立於虛擬機器外部,並且全部繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,類載入器可以劃分的更細緻一些,絕大部分Java程式都會使用到以下3種系統提供的類載入器。
– 啟動類載入器(Bootstrap ClassLoader):前面已經介紹過,這個類將存放在\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名稱不符合類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,那直接使用null代替即。
– 擴充套件類載入器(Extension ClassLoader):這個載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入器\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
– 應用程式類載入器(Application ClassLoader):這個類載入器由sun.misc.Launcher AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中的預設類載入器。

我們的應用程式都是由這3種類載入器互相配合進行載入的,如果有必要,還可以加入自己定義的類載入器。這些類載入器之間的關係一般如下圖。

image

上圖中展示的類載入器之間的這種層次關係,稱為類載入器的雙親委派模型。雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父載入器的程式碼。

類載入器的雙親委派模型在JDK1.2期間被引入並被廣泛應用於之後幾乎所有的Java程式中,但它並不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入實現方式。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的ClassPath中,那系統將會出現多個不同的Object類,Java型別體系中最基本的行為也就無法保證,應用程式也將會變得一片混亂。

雙親委派模型對於保證Java程式的穩定運作很重要,但它的實現確非常的簡單,實現雙親委派的程式碼都集中在java.lang.ClassLoader的loadClass()方法中,邏輯清晰易懂:先檢查是否已經被載入過,若沒有載入則呼叫父載入器的loadClass()方法,若父載入器為空則預設使用啟動類載入器作為父載入器。如果父類載入失敗,丟擲ClassNotFoundException異常後,再呼叫自己的findClass()方法進行載入。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

破壞雙親委派模型

上文提到過雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發的類載入器實現方式。在Java的世界中大部分的類載入器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現過3次大規模的“被破壞”情況。

雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK1.2釋出之前,由於雙親委派模型在JDK1.2之後才被引入,而類載入器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的使用者自定義類載入器的實現程式碼,Java設計者引入雙親委派模型時不得不做出一些妥協。為了向前相容,JDK1.2之後的java.lang.ClassLoader新增了一個新的protected方法findClass(),在此之前,使用者去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,因為虛擬機器在進行類載入的時候會呼叫載入器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去呼叫自己的loadClass()。

上一節我們已經看過loadClass()方法的程式碼,雙親委派的具體邏輯就實現在這個方法之中,JDK1.2之後已不提倡使用者去覆蓋loadClass()方法,而應當把自己的類載入邏輯寫到findClass()方法中,在loadClass()方法的邏輯裡如果父類載入失敗,則會呼叫自己的findClass()方法來完成載入,這樣就可以保證新寫出來的類載入器是符合雙親委派規則的。

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,雙親委派很好地解決了各個類載入器的基礎類的統一問題(越基礎的類越由上層的載入器進行載入),基礎類之所以稱為“基礎”,是因為它們總是作為被使用者程式碼呼叫的API,但世事往往沒有絕對的完美,如果基礎類又要呼叫回使用者的程式碼,那該怎麼辦?

這並非不可能的事情,一個典型的例子就是JNDI服務,JNDI現在已經是Java標準服務,它的程式碼由啟動類載入器去載入,但JNDI的目的就是對資源進行集中管理和查詢,它需要呼叫由獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI,Server Provider Interface)的程式碼,但啟動類載入器不可能“認識”這些程式碼啊!那該怎麼辦?

為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:執行緒上線文類載入器。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。

有了執行緒上線文類載入器,就可以做一些“舞弊”的事情了,JNDI服務使用這個執行緒上線文類載入器所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的層次結構類逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則,但這也就是無可能奈何的事情。Java中所有涉及SPI的載入動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

雙親委派模型的第三次“被破壞”是由於使用者對程式動態性的追求而導致的,這裡所說的“動態性”指的是當前一些非常“熱門”的名詞:程式碼熱替換、模組熱部署等,說白了就是希望應用程式能像我們的計算機外設那樣,接上滑鼠、U盤,不用重啟機器就能立即使用,滑鼠有問題或要升級就換一個滑鼠,不用停機也不用重啟。對於個人計算機來說,重啟一次其實沒有什麼大不了的,但對於一些生產系統來說,關機重啟一次可能就要被列入生產事故,這種情況下熱部署就對軟體開發者,尤其是企業級軟體開發者具有很大的吸引力。

Sun公司所提出的JSR-294、JSR-277規範在於JCP組織的模組化規範之爭中落敗給JSR-291,雖然Sun不甘失去Java模組化的主導權,獨立發展Jigsaw專案,但目前OSGi已經成為業界“事實上”的Java模組化標準,而OSGi實現模組化熱部署的關鍵則是它自定義的類載入器,當需要更換一個Bundle時,就把Bundle連同類載入器一期換掉以實現程式碼的熱替換。

在OSGi環境下,類載入器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加複雜的網狀結構,當收到類載入器請求時,OSGi將按照下面的順序進行類搜尋:

  1. 將以java.*開頭的類委派給父類載入器載入。
  2. 否則,將委派列表名單內的類委派給父類載入器載入。
  3. 否則,將Import列表中的類委派給Export這個類的Bundle的類載入器載入。
  4. 否則,查詢當前Bundle的ClassPath,使用自己的類載入器載入。
  5. 否則,查詢類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類載入器載入。
  6. 否則,查詢Dynamic Import列表的Bundle,委派給對應的Bundle的類載入器載入。
  7. 否則,查詢失敗。

上面的查詢順序中只有開頭兩點仍然符合雙親委派規則,其餘的類查詢都是在平級的類載入器中進行的。

更多Java乾貨文章請關注我的個人微信公眾號:老宣與你聊Java

這裡寫圖片描述