深入學習Java虛擬機:類加載機制與類加載器

Java虛擬機-類加載機制與類加載器

Java中類加載、連接和初始化的過程都是在程序運行期間完成的,這些策略雖然會令類加載時增加些性能開銷,但是會提高java的靈活性。Java動態擴展的特性就是依賴運行期動態加載和動態連接的特點實現的。

 

JVM類加載機制

Java源代碼被編譯爲字節碼文件後,需要加載進內存才能在程序中被使用。程序啓動時並不會一次性加載程序要用的所有class文件,而是根據程序需要,通過Java的類加載機制(ClassLoader)動態加載某個class文件到內存當中。ClassLoader就是用來在運行時加載字節碼文件進內存的,加載的過程是線程安全的(如何保證其是線程安全的?)。

Java默認提供三個ClassLoader,分別是BootStrap ClassLoaderExtension ClassLoaderApplication ClassLoader,默認使用雙親委派模式進行類加載。雙親委派只是個推薦,也可以不適用此策略,如Tomcat

啓動類加載器負責加載JDK的核心類庫,如rt.jarresources.jarcharsets.jar等,並構造擴展類加載器和系統類加載器,隨JVM啓動而啓動、

擴展類加載器,負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的所有jar

系統類加載器,負責加載應用程序classpath目錄下的所有jarclass文件。

用戶可自定義類加載器,通過繼承java.lang.ClassLoader類並重寫findClass()方法進行

 

類加載過程中,先後會對字節碼文件進行加載、連接(驗證、準備、解析)、初始化等步驟,這些步驟都在程序運行期間完成,最終類的描述信息存放在方法區(補充)。。。。。。。其中,解析階段執行時間不確定,某些情況下可以在初始化階段後纔開始,以支持Java語言運行時綁定機制(動態綁定或晚期綁定)

這就是類加載機制。

 

隨後,被加載的類將被使用、卸載,這就是類的生命週期。

 

類被加載到虛擬機內存中開始,到卸載爲止,整個生命週期包括:加載、連接(驗證、準備、解析)、初始化、使用和卸載階段

 

類加載發生時機:

1.虛擬機啓動

Java 虛擬機的啓動是通過引導類加載器(Bootstrap Class Loader)創建一個初始類(Initial Class)來完成,這個類是由虛擬機的具體實現指定。緊接着,Java 虛擬機鏈接這個初始類,初始化並調用它的 public void main(String[])方法。之後的整個執行過程都是由對此方法的調用開始。執行 main 方法中的 Java 虛擬機指令可能會導致Java 虛擬機鏈接另外的一些類或接口,也可能會調用另外的方法。

 

可能在某種 Java 虛擬機的實現上,初始類會作爲命令行參數被提供給虛擬機。當然,虛擬機實現也可以利用一個初始類讓類加載器依次加載整個應用。初始類當然也可以選擇組合上述的方式來工作。

 

2.類加載

 

3.虛擬機退出

Java 虛擬機的退出條件一般是:某些線程調用 Runtime 類或 System 類的 exit 方法,或是 Runtime 類的 halt 方法,並且 Java 安全管理器也允許這些 exit 或 halt 操作。除此之外,在 JNI(Java Native Interface)規範中還描述了當使用 JNI API 來加載和卸載(Load & Unload)Java 虛擬機時,Java 虛擬機的退出過程。

 

 

類裝載條件:主動使用

類何時觸發初始化?主動使用時觸發初始化

 

主動使用:Class只有在要使用到的時候纔會被裝載,進行初始化。使用是指主動使用.

 

僅以下情況屬於主動使用:

1.遇到newgestaticputstaticinvokestatic4條字節碼指令時,如果類沒有進行過初始化,需要觸發其初始化

當用new關鍵字實例化對象

調用類的靜態方法時。即當使用了字節碼invokestatic指令

用類或接口的靜態字段時,或對這些靜態字段執行賦值操作時(final常量、已在編譯器把結果放入常量池的靜態字段除外),如用了getstatic或putstatic指令 
2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。 

注:反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制,這相對好理解爲什麼需要初始化類。
3.當初始化一個類的時候,如果其父親還沒有進行過初始化,則需要觸發其父類的初始化。(繼承) 。接口除外
4.當虛擬機啓動時候,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。 (啓動類)
5.當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

 

被動引用:【被動使用不會引起裏的初始化】

1.通過子類引用父類的靜態字段,不會導致子類初始化。 

class SuperClass{

    static{

        System.out.println("SuperClass init!");

    }

    public static final int value = 123;

}

class ChildClass extends SuperClass{

    static{

        System.out.println("ChildClass init!");

    }

}

public class ConstClass {

    public static void main(String[] args){

        System.out.println(ChildClass.value);

    }

}

 

 

2.通過數組定義來引用類,不會觸發此類的初始化。 (實際初始化的是數組類,這個是由JVM自動生成,直接繼承Object的子類,創建動作由字節碼指令newarray觸發)

public class NotInitialization {

/**

 * 被動引用類字段演示二:

 * 通過數組定義來引用類,不回觸發此類的初始化。

 * 沒有輸出結果,說明數組的定義不會引用類,觸發初始化操作

 */

    public static void main(String[] args) {

        SuperClass[] sca = new SuperClass[10];

    }

}

 

3.類常量在編譯階段會存入調用類的常量池(編譯階段的常量傳播優化),本質上並無直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

/**

 * 被動引用類字段演示三:

 * 常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

 * 結果僅僅輸出 hello world ,而不會初始化ConstClass1 init! 靜態代碼塊;

 * 分析:因爲雖然在java源碼中引用了ConstClass1類中的常量HELLOWORLD,但是在編譯階段將此常量的值存儲到了NotInitialization1類的常量池中

 * 實際上最後引用的是自己的常量池中的常量,並不會引入ConstClass1的加載。

 */

class ConstClass1{

    static{

        System.out.println("ConstClass1 init!");

    }

    public static final String HELLOWORLD = "hello world";

}

public class NotInitialization1 {

    public static void main(String[] args) {

        System.out.println(ConstClass1.HELLOWORLD);

    }

}

 

4.接口的初始化:接口在初始化時,並不要求其父接口全部完成類初始化,只有在正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。(接口中沒有static{ } 語句塊,但編譯器任然會爲接口生成<clinit>()類構造器,用於初始化接口中所定義的成員變量)

接口和類的真正區別是接口是只有在正使用到父接口的時候(如引用接口中定義的常量)纔會初始化

 

 

非數組類的加載可以通過系統提供的引導類加載器完成,也可由用戶自定義類加載器完成,可以通過定義的類加載器去控制字節流的獲取方式(即重寫一個類加載器的loadClass()方法)

數組類本身不通過類加載器創建,由JVM直接創建的

數組類的創建過程遵循以下規則: 
  1.如果數組的組件類型(指的是數組去掉一個維度的類型)是引用類型,那就遞歸採用上面的加載過程去加載這個組件類型,數組c將加載該組件類型的類加載器的類名稱空間上被標識(一個類必須和類加載器一起確定唯一性)
  2.如果數組的組件類型不是引用類型(如int[]),Java虛擬機將會把數組c標識爲與引導類加載器關聯 
  3.數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認爲public 
 

 

類加載過程詳解:

類的加載指將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構

類加載的過程:類從被加載到虛擬機內存中開始,到卸載出內存爲止的過程:

整個生命週期包括:加載、鏈接、初始化。

加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。

 

以下內容以HotSpot爲基準。

1.加載(Loading):生成Class對象

獲取類的二進制流,轉爲方法區數據結構,在Java堆中生成對應的java.lang.Class對象。【找到.class文件並把這個文件包含的字節碼加載到內存中】,對於數組類來說,並沒有對應的字節流,而是由JVM直接生成的。

在加載階段(可參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下3件事情:

1、   通過一個類的全限定名來獲取定義此類的二進制字節流(並沒有指明要從一個Class文件中獲取,可從其他渠道,如:網絡(URLClassLoader)、動態生成、數據庫等);

2、   將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構

3、  在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口;

對象肯定是存放在堆中的,但Class對象比較特殊,對於HotSpot虛擬機而言,Class對象是存放在方法區中的。

 

Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口(反射)

Class類無參構造函數爲private。且只有java虛擬機纔可以創建class

 

鏈接:獲取二進制字節流的方式?

1)從zip包中讀取,最終成爲日後JAR、EAR、WAR格式的基礎 
  2)從網絡中獲取,典型的應用就是Applet 
  3)運行時計算生成,常見的是動態代理計技術。在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來爲特定接口生成形式爲“*$Proxy”的代理類的二進制字節流
  4)由其他文件生成,典型場景是JSP應用 。即由Jsp生成class類
  5)從數據庫中讀取,這種場景相對少一些(中間件服務器)

 

加載和連接階段(Linking)的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬於連接階段的內容,這兩個階段的開始時間仍然保持着固定的先後順序。

2.連接(Linking)

鏈接是指將創建成的類合併到JVM中,使之能執行的過程。

1)驗證(Verification)

驗證(安全的考慮,需要驗證)爲了保證class流的格式正確

爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

 

大致完成4個階段的檢驗動作:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證

文件格式驗證

文件格式驗證:驗證字節流是否符合Class文件格式的規範

如:

         1.是否以魔數開頭:0xCAFEBABE

         2.主、次版本號是否在當前虛擬機的處理範圍中

         3.常量池的常量中是否有不被支持的常量類型(檢驗常量tag標誌)

         4.指向常量中的各種索引值中是否有指向不存在的常量或不符合類型的常量

         5.CONSTANT_Utf8_infoz型的常量中是否有不符合UTF8編碼的數據

         6.Class文件中各個部分以及文件本身是否有被刪除的或附加的其他信息

         …(還有很多)

【這個驗證都是基於二進制字節流進行驗證,只有通過類這個階段的驗證後,字節流纔會進入內存的方法區進行存儲後面的3個驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流

元數據(語義)驗證

元數據(語義)驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求

如:這個類是否有父類。Fianl類型的方法/類是否被覆蓋,非抽象實現所有抽象方法。類中字段、方法是否與父類產生矛盾(如覆蓋類父類的final字段,或出現不符合規則的方法重載,如方法參數都一致,但返回值類型卻不同等) 

         1.是否有父類(除java.lang.Object外)

         2.是否繼承了不允許被繼承的類(被final修飾的類)

         3.如果不是抽象類,是否實現了父類中所有的抽象方法和接口中的所有方法

         4.是否覆蓋了父類的final字段或方法重載不符合規則

 

【主要目的是對類元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息】

字節碼驗證

字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的

如運行檢查。棧數據類型和操作碼數據參數吻合,跳轉指令到正確合理的位置。保證方法體中的類型轉換時有效的(避免子類new父類)【對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件】

         1.保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作(比如不會出現在操作數棧防了一個int型的數據卻按long類型來加載到本地變量表中)

         2.保證跳轉指令不會跳到方法體外的字節碼指令上

         3.保證類型轉化是有效的

         但即使進行了大量的分析也不能保證字節碼就是安全的,涉及到了著名的停機問題

 

由於數據流校驗的高複雜性,耗時較大,JDK1.6後,在Javac中引入一項優化方法(可以通過參數-XX:-UseSplitVerifier關閉這種優化):在方法體的Code屬性的屬性表中增加一項“StackMapTable”屬性,該屬性描述了方法體中所有基本塊開始時本地變量表和操作棧應有的狀態,在字節碼驗證階段,就不需要根據程序推到這些狀態的合法性,只需要檢查StackMapTable屬性中的記錄是否合法即可。從而將字節碼驗證的類型推導轉變爲類型檢查從而節省一些時間。

JDK1.7後,對於主版本號大於50的class文件,使用類型檢查來完成數據分析校驗是唯一選擇,不允許再退回到類型推倒的校驗方式。

 

注:理論上StackMapTable存在錯誤或被篡改的可能,有可能code屬性被修改了,然後StackMapTable也被篡改,欺騙jvm的校驗。

符號引用驗證

符號引用(二進制兼容)驗證:確保解析動作能正確執行。如work類引用了cat對象並調用其run方法,需判斷是否有run方法,沒有則拋出NoSuchMehodError異常。如常量池中描述類是否存在,訪問的方法或字段是否存在足夠的權限

檢驗內容有:

1.符號引用中通過字符串描述的全限定名是否能找到對應的類;

2.在指定類中是否存在符號方法的字段描述及簡單名稱所描述的方法和字段;

3.符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。

 

驗證階段非常重要,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,可考慮-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間

2)準備(Preparation): 爲靜態變量分配內存並設置默認的初始值

正式爲類變量分配內存並設置類變量初始值的階段,這些變量所用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在堆中(分配空間並進行默認值初始化)。【類變量在方法區。實例變量在堆區】

這裏的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義爲:

 

public static int value=123;

變量value在準備階段過後的初始值爲0而不是123.因爲這時尚未開始執行任何java方法,把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把value賦值爲123的動作在初始化階段纔會執行。
至於“特殊情況”是指:

public static final int value=123,

當類字段的屬性是ConstantValue時,會在準備階段初始化爲指定的值,標註爲final後,value的值在準備階段初始化爲123而非0.

 

3).解析(Resolution): 將符號引用替換爲直接引用

虛擬機將常量池內的符號引用(也就是字符串)替換爲直接引用(指針或者地址偏移量)的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

虛擬機規範中併爲規定解析階段發生的具體時間,只要求了再執行Java 虛擬機指令 anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 這16個用於操作符號的引用的字節碼指令之前,先對他們所使用的符號引用進行解析。也就是執行上述任何一條指令都需要對它的符號引用的進行解析。所以:虛擬機實現可以根據需要來判斷到底是在類被加載器加載時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才取解析它。

 

對同一個符號引用進行多次解析請求是很常見的事情,虛擬機實現可以對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態)從而避免解析動作重複進行。

對於invokedynamic指令,上面規則則不成立。當碰到某個前面已經由invokedynamic指令觸發過解析的符號引用時,並不意味着這個解析結果對其他invokedynamic指令也同樣生效。因爲invokedynamic指令是JDK1.7新加入的指令,目的用於動態語言支持,它所對應的引用稱爲“動態調用點限定符”(Dynamic Call Site Specifier),這裏“動態”的含義就是必須等到程序實際運行到這條指令的時候,解析動作才能進行。相對的,其餘可觸發解析的指令都是“靜態”的,可以在剛剛完成加載階段,還沒有執行代碼時就進行解析。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號進行引用,下面只對前4種引用的解析過程進行介紹,對於後面3種與JDK1.7新增的動態語言支持息息相關

 

 

類或接口的解析

        假設當前代碼所處的類爲D,如果要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,虛擬機完成整個解析的過程需要以下3個步驟:

1.      如果C不是一個數組類型,虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程中,由於元數據驗證、字節碼驗證的需要,又可能觸發其他相關類的加載動作,例如加載這個類的父類或實現的接口。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。

2.      如果C是一個數組類型,並且數組的元素類型爲對象,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第1點的規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載的元素類型就是“java.lang.Integer”,接着由虛擬機生成一個代表此數組維度和元素的數組對象。

3.      如果上面的步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證,確認D是否具備對C的訪問權限。如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError異常。

 

字段解析

        要解析一個未被解析過的字段符號引用,首先將會對字段表內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 FieldResolution {
    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);
    }
}

類方法解析

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

1.      類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。

2.      如果通過了第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。

3.      否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。

4.      否則,在類C實現的接口列表及他們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象,這時查找結束,拋出java.lang.AbstractMethodError異常。

5.      否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。

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

 

接口方法解析

        接口方法也需要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功,依然用C表示這個接口,接下來虛擬機將會按照如下步驟進行後續的接口方法搜索。

1.      與類方法解析不同,如果在接口方法表中發現class_index中的索引C是個類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。

2.      否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。

3.      否則,在接口C的父接口中遞歸查找,直到java.lang.Object(查找範圍會包括Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。

4.      否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

5.      由於接口中的所有方法默認都是public,所以不存在訪問權限的問題,因此接口方法的符號解析應當不會拋出java.lang.IllegalAccessError異常。

 

 

關於符號引用和直接引用:

1)符號引用(Symbolic References):

符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須都是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。

 

2)直接引用(Direct References):

直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接點位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在

 

解讀一下:就拿以上截圖的紅色框框的例子來舉例吧,框住的常量池語意大概是常量池中的第三個常量爲類或接口的符號引用,這個符號的值爲第四個常量池的值,也就是“java/lang/Object;”這是我們熟知的Object類的全限定名。解析階段就是要把這個“class”的字符引用換成直接指向這個Object類在內存中的地址(如指針 )。那就說明,這個Object類必須同時也需要加載到內存中來。

 

 

3.初始化(Initialization):真正執行Jaca代碼

【類中靜態屬性和初始化賦值、靜態塊的執行等】

初始化階段,才真正開始執行類中定義的java程序代碼。初始化階段是執行類構造器<clinit>()方法的過程

靜態變量的初始化由兩種途徑:

  1. 在靜態變量聲明處進行初始化
  2. 在靜態代碼塊進行初始化

 

1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;

2)如果類中存在初始化語句,就依次執行這些初始化語句

 

在連接的準備階段,類變量已賦過一次系統要求的初始值,在初始化階段,則是根據邏輯去初始化類變量和其他資源,如下:

    public static int value1  = 5;

    public static int value2  = 6;

    static{

        value2 = 66;

    }

在準備階段value1value2都等於0

在初始化階段value1value2分別等於566

 

<clinit>()方法由編譯期自動收集類的所有類變量的賦值動作和靜態語句塊中的語句合併產生的,

編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量;定義在它之後的變量,在前面的靜態語句塊可賦值,但不能訪問

非法前向引用:

public static int a = b;  // 報錯,非法前向引用。交換順序就不會爆錯了
public static int b =10;

// 非法向前引用

//   static int i=1;

    static {

      i=0;

      System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前引用)。註釋掉就不會報錯了,i結果是1

    }

    static int i=1;

 

 

初始化階段是執行類構造器<clinit>()方法的過程.

所有類變量初始化語句和靜態代碼塊都會在編譯時被編譯器放在收集器裏頭,存放到一個特殊的方法<clinit>方法中,即類/接口的初始化方法,該方法只能在類加載的過程中由JVM調用

子類的<clinit>調用前會保證父類的<clinit>被調用,在<clinit>方法內部不會顯示調用超類的<clinit>方法,JVM負責保證一個類的<clinit>方法執行之前,它的超類<clinit>方法已經被執行

 

()方法與實例構造器<init>()方法不同,不需要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行之前,父類的<clinit>()方法方法已經執行完畢

由於父類的<clinit>()方法先執行,意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作。

<clinit>()方法對於類或者接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,編譯器可不爲這個類生產<clinit>()方法。如果一個類沒有聲明任何的類變量(static),也沒有靜態代碼塊,那麼可沒有類<clinit>方法;

接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法

<clinit>是線程安全的,JVM會保證類的<clinit>()方法在多線程環境中被正確地加鎖。同步,只允許一個線程對其執行初始化操作,其他等待。初始化完成後才通知其他線程,((所以可利用靜態內部類實現線程安全的單例模式如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的。【同一個類加載器下,一個類型只會初始化一次】

 

    static class DeadLoopClass{

        static  {

            if(true){

                System.out.println(Thread.currentThread()+"init DeadLoopClass");

                while(true){

                }

            }

        }

    }

    public static void main(String[] args)   {

        Runnable script = new Runnable(){

            public void run()  {

                System.out.println(Thread.currentThread()+" start");

                DeadLoopClass dlc = new DeadLoopClass();

                System.out.println(Thread.currentThread()+" run over");

            }

        };

        Thread thread1 = new Thread(script);

        Thread thread2 = new Thread(script);

        thread1.start();

        thread2.start();

    }

運行結果:(即一條線程在死循環以模擬長時間操作,另一條線程在阻塞等待)

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-0,5,main]init DeadLoopClass

需要注意的是,其他線程雖然會被阻塞,但如果執行()方法的那條線程退出()方法後,其他線程喚醒之後不會再次進入()方法。同一個類加載器下,一個類型只會初始化一次。

 

 

 

幾種方法獲取Class對象:

調用對象的getClass方法。

調用Class.forName()方法,參數爲類的全名

使用.class屬性獲取Class對象

 

 

JVM類加載器機制與類加載過程 

Java虛擬機啓動、加載類過程分析

package org.luanlouis.jvm.load;  

import sun.security.pkcs11.P11Util;  

public class Main{  

    public static void main(String[] args) {  

        System.out.println("Hello,World!");  

        ClassLoader loader = P11Util.class.getClassLoader();  

        System.out.println(loader);  

    }  

}  

命令行下輸入:  java    org.luanlouis.jvm.load.Main

當輸入上述的命令時: java.exe 程序將完成以下步驟: 

1.  根據JVM內存配置要求,爲JVM申請特定大小的內存空間;

2.  創建一個引導類加載器實例,初步加載系統類到內存方法區區域中;

3.   創建JVM 啓動器實例 Launcher,並取得類加載器ClassLoader

4.  使用上述獲取的ClassLoader實例加載我們定義的 org.luanlouis.jvm.load.Main類;

5.  加載完成時JVM會執行Main類的main方法入口,執行Main類的main方法

6.  結束,java程序運行結束,JVM銷燬。

 

1.根據JVM內存配置要求,爲JVM申請特定大小的內存空間

JVM啓動時,JVM內存按功能劃分,粗略地劃分爲方法區(Method Area) 和堆(Heap),所有的類的定義信息都會被加載到方法區中。

 

2.創建一個引導類加載器實例,初步加載系統類到內存方法區區域中;

JVM申請好內存空間後,JVM會創建一個引導類加載器(Bootstrap Classloader)實例負責加載JVM虛擬機運行時所需的基本系統級別的類,如java.lang.String, java.lang.Object等等。

Bootstrap Classloader會讀取 {JRE_HOME}/lib 下的jar包和配置,將這些系統類加載到方法區

可使用參數 -Xbootclasspath  系統變量sun.boot.class.path來指定的目錄來加載類。

 

一般,{JRE_HOME}/lib下存放着JVM正常工作所需要的系統類,如下表所示:

文件名

描述

rt.jar

運行環境包,rt即runtime,J2SE 的類定義都在這個包內

charsets.jar

字符集支持包

jce.jar

一組包,提供加密、密鑰生成和協商以及 Message Authentication Code(MAC)算法的框架和實現

jsse.jar

安全套接字拓展包Java(TM) Secure Socket Extension

classlist

該文件內表示是引導類加載器應該加載的類的清單

net.properties

JVM 網絡配置信息

 

Bootstrap ClassLoader加載系統類後,JVM內存會呈現如下格局:

引導類加載器將類信息加載到方法區中,以特定方式組織,對某一個特定的類而言,在方法區中應該有運行時常量池、類型信息、字段信息、方法信息、類加載器的引用,對應class實例的引用等信息

類加載器的引用:由於這些類由引導類加載器(Bootstrap Classloader)加載的,BootstrapC++實現,無法訪問,故而該引用爲NULL

對應class實例的引用:類加載器在加載類信息放到方法區中後,會創建一個對應的Class 類型的實例放到堆(Heap), 作爲訪問方法區中類定義的入口和切入點

 

測試:在代碼中嘗試獲取系統類如java.lang.Object的類加載器時,你會始終得到NULL:

System.out.println(String.class.getClassLoader());//null  

System.out.println(Object.class.getClassLoader());//null  

System.out.println(Math.class.getClassLoader());//null  

System.out.println(System.class.getClassLoader());//null  

 

3.創建JVM 啓動器實例 Launcher,並取得類加載器ClassLoader

上述步驟完成,JVM基本運行環境就準備就緒了。

要讓JVM工作:運行我們定義的程序 org.luanlouis,jvm.load.Main。

此時,JVM虛擬機調用已經加載在方法區的類sun.misc.Launcher 的靜態方法getLauncher(),  獲取sun.misc.Launcher 實例

sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher(); //獲取Java啓動器  

ClassLoader classLoader = launcher.getClassLoader();          //獲取類加載器ClassLoader用來加載class到內存來  

 

sun.misc.Launcher 單例,保證一個JVM虛擬機內只有一個sun.misc.Launcher實例
Launcher內部,定義了兩個類加載器(ClassLoader),

分別是sun.misc.Launcher.ExtClassLoader(拓展類加載器(Extension ClassLoader))和sun.misc.Launcher.AppClassLoader(應用類加載器(Application ClassLoader))

https://img-blog.csdn.net/20160117130521757

指向引導類加載器的虛線表示類加載器的這個有限的訪問 引導類加載器 的功能

 

除了Bootstrap ClassLoader的所有類加載器,都能判斷某一個類是否被引導類加載器加載過,如果加載過,直接返回對應的Class<T> instance,如果沒有,則返回null. 

 launcher.getClassLoader() 會返回 AppClassLoader 實例,AppClassLoader將ExtClassLoader作爲父加載器。

 

 

4.使用類加載器加載Main類

通過 launcher.getClassLoader()方法返回AppClassLoader實例,

接着就是AppClassLoader加載 org.luanlouis.jvm.load.Main類的時候了。

ClassLoader classloader = launcher.getClassLoader();//取得AppClassLoader類  

classLoader.loadClass("org.luanlouis.jvm.load.Main");//加載自定義類  

 

定義的org.luanlouis.jvm.load.Main類被編譯成class二進制文件,這個class文件中有一個叫常量池(Constant Pool)的結構體來存儲該class的常量信息。

常量池中有CONSTANT_CLASS_INFO類型的常量,表示該class中聲明瞭要用到那些類:

當AppClassLoader要加載 org.luanlouis.jvm.load.Main類時,會去查看該類的定義,發現它內部聲明使用了其它的類: sun.security.pkcs11.P11Util、java.lang.Object、java.lang.System、java.io.PrintStream、java.lang.Class;

org.luanlouis.jvm.load.Main類要想正常工作,首先要能夠保證這些其內部聲明的類加載成功。

AppClassLoader要先將這些依賴類加載到內存。(注:爲理解方便,沒有考慮懶加載情況,事實上的JVM加載類過程比這複雜的多

 

加載過程(雙親委派模式):

1. 加載java.lang.Object、java.lang.System、java.io.PrintStream、java,lang.Class

AppClassLoader嘗試加載這些類的時候,會先委託ExtClassLoader進行加載;

ExtClassLoader發現不是其加載範圍,其返回null;

AppClassLoader發現父類加載器ExtClassLoader無法加載,會查詢這些類是否已經被BootstrapClassLoader加載過,結果表明這些類已經被BootstrapClassLoader加載過,則無需重複加載,直接返回對應的Class<T>實例;

2. 加載sun.security.pkcs11.P11Util。在{JRE_HOME}/lib/ext/sunpkcs11.jar包內,屬於ExtClassLoader負責加載的範疇。

AppClassLoader嘗試加載這些類的時候,會先委託ExtClassLoader進行加載;

ExtClassLoader發現其正好屬於加載範圍,故ExtClassLoader負責將其加載到內存中。ExtClassLoader在加載sun.security.pkcs11.P11Util時也分析這個類內都使用了哪些類,並將這些類先加載內存後,纔開始加載sun.security.pkcs11.P11Util,加載成功後直接返回對應的Class<sun.security.pkcs11.P11Util>實例;

3. 加載org.luanlouis.jvm.load.Main

  AppClassLoader嘗試加載這些類的時候,先委託ExtClassLoader進行加載;ExtClassLoader發現不是其加載範圍,其返回null;AppClassLoader發現父類加載器ExtClassLoader無法加載,則會查詢這些類是否已經被BootstrapClassLoader加載過。而結果表明BootstrapClassLoader 沒有加載過它,這時AppClassLoader只能自己動手負責將其加載到內存中,然後返回對應的Class<org.luanlouis.jvm.load.Main>實例引用;

以上三步驟都成功,才表示classLoader.loadClass("org.luanlouis.jvm.load.Main")完成,

上述操作完成後,JVM內存方法區的格局會如下所示:

JVM方法區的類信息區是按照類加載器進行劃分的,每個類加載器會維護自己加載類信息;

某個類加載器在加載相應的類時,會相應地在JVM內存堆(Heap)中創建一個對應的Class<T>,用來表示訪問該類信息的入口

 

5. 使用Main類的main方法作爲程序入口運行程序

6. 方法執行完畢,JVM銷燬,釋放內存

 

 

關於類加載器

類加載器主要通過一個類的全限定名來獲取描述此類的二進制字節流。

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

 

類加載器種類及其組織結構

類加載器(Class Loader):指的是可加載類的工具。

JVM自身定義了三個類加載器:

引導類加載器(Bootstrap Class Loader)、

拓展類加載器(Extension Class Loader )、

應用加載器(Application Class Loader)。

1. 引導類加載器(Bootstrap Class Loader)

使用C/C++底層代碼實現的加載器,用以加載JVM運行時所需要的系統類,這些系統類在{JRE_HOME}/lib目錄下。由於類加載器是使用平臺相關的底層C/C++語言實現的,所以該加載器不能被Java代碼訪問到。但是可查詢某個類是否被引導類加載器加載過。【負責加載JVM基礎核心類庫(rt.jar)】

經常使用的系統類如:java.lang.String,java.lang.Object,java.lang*....... 這些都被放在 {JRE_HOME}/lib/rt.jar包內, 當JVM系統啓動的時候,引導類加載器會將其加載到 JVM內存的方法區中。

 

用Bootstrcp ClassLoader來加載自定義類,有兩種方式:

1、在jvm中添加-Xbootclasspath參數,指定Bootstrcp ClassLoader加載類的路徑,並追加我們自已的jar(ClassTestLoader.jar)

2、將class文件放到JAVA_HOME/jre/classes/目錄下(上面有提到)

http://hi.csdn.net/attachment/201202/25/0_1330175588R53g.gif

 

2. 拓展類加載器(Extension Class Loader)

加載 java 的{JRE_HOME}/lib/ext/ 目錄下的拓展類 ,用來提供除了系統類外的額外功能。是整個JVM加載器的Java代碼可訪問到的類加載器的最頂端,即超級父加載器,拓展類加載器是沒有父類加載器的。

3. 應用類加載器(Applocatoin Class Loader)

用於加載用戶代碼,是用戶代碼的入口。

經常執行指令 java   xxx.x.xxx.x.x.XClass , 實際上,JVM就是使用的AppClassLoader加載 xxx.x.xxx.x.x.XClass 類的。應用類加載器將拓展類加載器當成自己的父類加載器,當其嘗試加載類的時候,首先嚐試讓其父加載器-拓展類加載器加載;如果拓展類加載器加載成功,則直接返回加載結果Class<T> instance,加載失敗,則會詢問是否引導類加載器已經加載了該類;只有沒有加載的時候,應用類加載器纔會嘗試自己加載。由於xxx.x.xxx.x.x.XClass是整個用戶代碼的入口,在Java虛擬機規範中,稱其爲 初始類(Initial Class).

4. 用戶自定義類加載器(Customized Class Loader):

用戶可自己定義類加載器來加載類。所有的類加載器都要繼承java.lang.ClassLoader類。

 

關係:ExtClassLoader 和 AppClassLoader 都繼承URLClassLoader 類,而URLClassLoader又實現了抽象類ClassLoader,在創建Launcher對象時首先創建ExtClassLoader,然後將ExtClassLoader對象作爲父加載器創建AppClassLoader對象,而通過Launcher.getClassLoader()方法獲取的ClassLoader就是AppClassLoader對象。所以如果在Java應用中沒有定義其他ClassLoader,那麼除了 System.getProperty("java.ext.dirs”)目錄下的類是由ExtClassLoader加載外,其他類都由AppClassLoader來加載

 

 

雙親委派模型(parent-delegation model):

雙親委派模式加載類:

當AppClassLoader加載類時,會先嚐試讓父加載器ExtClassLoader進行加載,

如果父加載器ExtClassLoader加載成功,則AppClassLoader直接返回父加載器ExtClassLoader加載的結果;

如果父加載器ExtClassLoader加載失敗,AppClassLoader則會判斷該類是否是通過Bootstrap類加載器加載,會調用native方法進行查找;

若要加載的類不是系統引導類,AppClassLoader將嘗試自己加載,加載失敗將會拋出“ClassNotFoundException”。

 

AppClassLoader的工作流程如下所示:(雙親委派模型)

https://img-blog.csdn.net/20160119193940660

這是JDK自身默認的加載類的行爲,可通過繼承複寫方法,改變其行爲。

 

對於某個特定的類加載器而言,應該爲其指定一個父類加載器,當用其進行加載類的時候:

(雙親委派模式流程總結)

1. 委託父類加載器幫忙加載;
2. 父類加載器加載不了,查詢引導類加載器有沒有加載過該類;
3. 如果引導類加載器沒有加載過該類,則當前的類加載器應該自己加載該類;
4. 若加載成功,返回對應的Class<T> 對象;若失敗,拋出異常“ClassNotFoundException”

總結:只有當父加載器反饋自己無法完成加載請求時,子加載器纔會嘗試自己加載。

 

注意:
雙親委派模型中的"雙親"並不是指它有兩個父類加載器,一個類加載器只應該有一個父加載器。

而是有兩個角色:
1. 父類加載器(parent classloader):可替子加載器嘗試加載類(真爸爸)
2. 引導類加載器(bootstrap classloader: 子類加載器只能判斷某個類是否被引導類加載器加載過,而不能委託它加載某個類(乾爹);就是子類加載器不能接觸到引導類加載器,引導類加載器對其他類加載器而言是透明的。
 

 

ClassLoader使用雙親委託模型來搜索類的,每個ClassLoader實例有一個父類加載器的引用(不是繼承的關係,是包含的關係),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可用作其它ClassLoader實例的的父類加載器。

http://hi.csdn.net/attachment/201202/25/0_13301699801ybH.gif

 

好處:

1.java類隨着它的加載器一起舉杯了一種帶有優先級的層次關係,如Object,存在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂層的啓動類加載器進行加載的,因此Object在程序的各種類加載器環境中都是同一個類,如果不是雙清模式,就可能出現多個不同的Object類,java體系最基礎的行爲也無法保證。

2.避免重複加載:當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次(杜絕冒充)

壞處:

如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲java.lang.object的類,並放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行爲也就無法保證,應用程序也將會變得一片混亂

 

Q:爲什麼要使用雙親委託這種模型呢?(防止多次載入)

       這樣可避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。如果不使用這種委託模式,就可隨時使用自定義的StringObject來動態替代java核心api中定義的類型,就會存在非常大的安全隱患,雙親委託的方式可避免這種情況,因爲String/Object已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,用戶自定義的ClassLoader永遠也無法加載一個自己寫的String/Object,除非改變JDK中ClassLoader搜索類的默認算法。

 

雙親模式是默認的模式,但是不是必須這樣做。如:Tomcat的WebappClassLoader會先加載自己的class,找不到再委託父類

 

Q:JVM在搜索類的時候,如何判定兩個class是相同的呢?

JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。只有兩者同時滿足,JVM才認爲這兩個class是相同的。

就算兩個class是同一份class字節碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認爲它們是兩個不同class。

 

 

雙親加載模型的邏輯和底層代碼實現:(JDK1.8)

java.lang.ClassLoader的核心方法 loadClass()的實現:

先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器,如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載

//提供class類的二進制名稱表示,加載對應class,加載成功,則返回表示該類對應的Class<T> instance 實例  

public abstract class ClassLoader {

public Class<?> loadClass(String name) throws ClassNotFoundException {

    return loadClass(name, false);

}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{

    synchronized (getClassLoadingLock(name)) {

        // First, check if the class has already been loaded,如果已經被加載,直接返回對應的Class<T>實例

        Class<?> c = findLoadedClass(name);

        if (c == null) {  //  初次加載  

            long t0 = System.nanoTime();
// 雙親委託

            try {

                if (parent != null) { //如果有父類加載器,則先讓父類加載器加載

                    c = parent.loadClass(name, false);  

                } else {// 沒有父加載器(Extension ClassLoader),則查看是否已經被引導類加載器加載,有則直接返回

                    c = findBootstrapClassOrNull(name);

                }

            } catch (ClassNotFoundException e) { 

            } 
// 父加載器加載失敗,並且沒有被引導類加載器加載,則嘗試該類加載器自己嘗試加載  

            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;

    }

}
protected final Class<?> findLoadedClass(String name) {

    if (!checkName(name))   return null;

    return findLoadedClass0(name);

}
private native final Class<?> findLoadedClass0(String name);
private boolean checkName(String name) {

    if ((name == null) || (name.length() == 0))

        return true;

    if ((name.indexOf('/') != -1) || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))

        return false;

    return true;

}
private Class<?> findBootstrapClassOrNull(String name){

    if (!checkName(name)) return null;

    return findBootstrapClass(name);

}

private native Class<?> findBootstrapClass(String name);
protected final void resolveClass(Class<?> c) {

    resolveClass0(c);

}

private native void resolveClass0(Class<?> c);

loadClass用於實現類加載器間的架構

findClass用於根據name尋找字節流,並調用defineClass將字節流轉換爲Class

 

 

類加載器與Class<T>  實例的關係

https://img-blog.csdn.net/20160123160945853

 

 

類加載器ClassLoader:

ClassLoader抽象類負責將Class加載到JVM中,審查每個類應該由誰加載,是一種父優先的等級加載機制(雙親委派模型),還有一個任務就是將Class字節碼重新解析成JVM統一要求的對象格式

 

ClassLoader可定製,滿足不同的字節碼流獲取方式

ClassLoader負責類裝載過程中的加載階段

 

defineClass():將byte字節流解析成JVM能夠識別的class對象。可通過class文件實例化對象,還可通過其他方式實例化對象,如通過網絡接收到一個類的字節碼,拿這個字節碼流直接創建類的Class對象形式實例化對象。

如果直接調用這個方法生成類的Class對象,這個類的Class對象還沒有resolve,這個resolve將會在這個對象真正實例化時才進行。

defineClass通常是和findClass方法一起使用的,通過直接覆蓋ClassLoader父類的fmdClass方法來實現類的加載規則,從而取得要加載類的字節碼。然後調用defineClass方法生成類的Class對象,如果想在類被加載到JVM中時就被鏈接(Link),那麼可接着調用另外一個resolveClass方法,當然也可選擇讓JVM來解決什麼時候才鏈接這個類。

可用this.getClass().getClassLoader().loadClass(“class”)調用ClassLoader的loadClass方法獲取這個類的Class對象。

 

幾個關鍵方法:

1loadClass : 此方法負責加載指定名字的類,ClassLoader的實現方法爲先從已經加載的類中尋找,如沒有則繼續從parent ClassLoader中尋找,如仍然沒找到,則從System ClassLoader中尋找,最後再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法

2findLoadedClass : 此方法負責從當前ClassLoader實例對象的緩存中尋找已加載的類,調用的爲native的方法。

3findClass : 此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。

4findSystemClass : 此方法負責從System ClassLoader中尋找類,如未找到,則繼續從Bootstrap ClassLoader中尋找,如仍然爲找到,則返回null

5defineClass : 此方法負責將二進制的字節碼轉換爲Class對象

6resolveClass : 此方法負責完成Class對象的鏈接,如已鏈接過,則會直接返回。

 

 

自定義ClassLoader:

不管是直接實現抽象類ClassLoader,還是繼承URLClassLoader類,或其他子類,它的父加載器都是AppClassLoader,因爲不管調用哪個父類構造器,創建的對象都必須最終調用getSystemClassLoader()作爲父加載器。而getSystemClassLoader()方法獲取到的正是 AppClassLoader。

 

爲什麼還要自定義類加載器呢?

Java中提供的默認ClassLoader,只加載指定目錄下的jar和class,如果想加載其它位置的類或jar時,如:要加載網絡上的一個class文件,通過動態加載到內存之後,要調用這個類中的方法實現自己的業務邏輯。這樣的情況下,默認的ClassLoader就不能滿足需求,需要定義自己的ClassLoader。

定義自已的類加載器分爲兩步:

1、繼承java.lang.ClassLoader

2、重寫父類的findClass方法,根據參數指定類的名字,返回對應的Class對象的引用

 

JDK已經在loadClass方法中實現了ClassLoader搜索類的算法,當在loadClass方法中搜索不到類時,loadClass方法會調用findClass方法來搜索類,只需重寫該方法即可。沒有特殊要求,不建議重寫loadClass搜索類的算法。

 

ClassLoader的loadClass方法:

protected Class<?> loadClass​(String name, boolean resolve) throws ClassNotFoundException

name:該類的 binary name 、resolve:如果 true然後解析該類

 

加載指定的類別binary name 。 此方法的默認實現按以下順序搜索類: (ClassLoader默認搜索算法·)

1、調用findLoadedClass(String)以檢查類是否已經加載。

2、在父類加載器上調用loadClass方法。 如果父級是null ,則使用內置到虛擬機中的類加載器。

3、調用findClass(String)方法來查找類。

如果使用上述步驟找到該類,並且resolve標誌爲真,則該方法將調用resolveClass(Class)方法對所生成的Class對象。

鼓勵ClassLoader子類覆蓋findClass(String) ,而不是這種方法。

除非被覆蓋,否則該方法在整個類加載過程中同步getClassLoadingLock方法的結果。

 

 

自定義加載類:

 public class NetworkClassLoader extends ClassLoader {  

    private String rootUrl;  

    public NetworkClassLoader(String rootUrl) {  

        this.rootUrl = rootUrl;  

    }  

    @Override  

    protected Class<?> findClass(String name) throws ClassNotFoundException {  

        Class clazz = null;//this.findLoadedClass(name); // 父類已加載     

        //if (clazz == null) {  //檢查該類是否已被加載過  

            byte[] classData = getClassData(name);  //根據類的二進制名稱,獲得該class文件的字節碼數組  

            if (classData == null)    throw new ClassNotFoundException(); 

            clazz = defineClass(name, classData, 0, classData.length);  //將class的字節碼數組轉換成Class類的實例  

        //}   

        return clazz;  

    }  

    private byte[] getClassData(String name) {  

        InputStream is = null;  

        try {  

            String path = classNameToPath(name);  

            URL url = new URL(path);  

            byte[] buff = new byte[1024*4];  

            int len = -1;  

            is = url.openStream();  

            ByteArrayOutputStream baos = new ByteArrayOutputStream();  

            while((len = is.read(buff)) != -1) {  

                baos.write(buff,0,len);  

            }  

            return baos.toByteArray();  

        } catch (Exception e) {  

            e.printStackTrace();  

        } finally {  

            if (is != null) {  

               try {  

                  is.close();  

               } catch(IOException e) {  

                  e.printStackTrace();  

               }  

            }  

        }  

        return null;  

    }  

    private String classNameToPath(String name) {  

        return rootUrl + "/" + name.replace(".", "/") + ".class";  

    }  

}  

測試類:

 try {  

            String rootUrl = "http://localhost:8080/httpweb/classes";  

            NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);  

            String classname = "org.classloader.simple.NetClassLoaderTest";  

            Class clazz = networkClassLoader.loadClass(classname);  

            System.out.println(clazz.getClassLoader());  

        } catch (Exception e) {  

            e.printStackTrace();  

        }  

 

 

常用web服務器中都定義了自己的類加載器,用於加載web應用指定目錄下的類庫(jarclass,如:Weblogic、Jboss、tomcat等,

 

 

雙親委派模式的被破壞

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

 

雙親委派的具體邏輯就實現在loadClass()方法之中,JDK 1.2後已不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯裏如果父類加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規則的。

 

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),但基礎類想要調用用戶類的代碼如何實現?。可以通過JNDI服務,它的代碼由啓動類加載器加載,JNDI的目的就是對資源進行集中管理和查找。需要調用獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI、Service Provider Interface)代碼。但啓動類加載器不認識這些代碼。通過線程上下文加載器去解決。JNDI服務通過使用線程上下文類加載器去加載所需要的SPI代碼。也就是父類加載器請求子類加載器去完成類加載動作。打破了雙清委派結構來逆向使用類加載器。

Java中所有涉及SPI的加載動作都採用這種方式,如JNDI、JDBC、JCE、JAXB和JBI等

線程上下文加載器

Java 任何一段代碼的執行,都有對應的線程上下文。如果在代碼中,想看當前是哪一個線程在執行當前代碼

使用如下方法:

Thread  thread = Thread.currentThread();//返回對當當前運行線程的引用  

https://img-blog.csdn.net/20160123173750315

可爲當前的線程指定類加載器。當執行 java  org.luanlouis.jvm.load.Main時,JVM會創建一個Main線程,而創建應用類加載器AppClassLoader的時候,會將AppClassLoader設置成Main線程的上下文類加載器:

  public Launcher() {  

      Launcher.ExtClassLoader var1;  

      try {  

          var1 = Launcher.ExtClassLoader.getExtClassLoader();  

      } catch (IOException var10) {  

          throw new InternalError("Could not create extension class loader", var10);  

      }  

      try {  

          this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);  

      } catch (IOException var9) {  

          throw new InternalError("Could not create application class loader", var9);  

      }  

//將AppClassLoader設置成當前線程的上下文加載器  

      Thread.currentThread().setContextClassLoader(this.loader);  

      //.......  

  }  

線程上下文類加載器是從線程的角度來看待類的加載,爲每一個線程綁定一個類加載器,可將類的加載從單純的雙親加載模型解放出來,進而實現特定的加載需求

 

 

雙親委派模型的第三次“被破壞”是由於用戶對程序動態性的追求而導致的, “動態性”指代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等,

目前OSGi已經成爲了業界“事實上”的Java模塊化標準,而OSGi實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(OSGi中稱爲Bundle)都有一個自己的類加載器,當需要更換一個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)否則,類查找失敗。

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

OSGi中對類加載器的使用是很值得學習的,弄懂了OSGi的實現,就可以算是掌握了類加載器的精髓。

 

熱部署與熱加載

在應用運行的時升級軟件,無需重新啓動的方式有兩種,熱部署和熱加載。

對於Java應用程序來說,熱部署就是在服務器運行時重新部署項目,熱加載即在在運行時重新加載class,從而升級應用。

實現原理:熱加載的實現原理主要依賴java的類加載機制,在實現方式可概括爲在容器啓動的時候起一條後臺線程,定時的檢測類文件的時間戳變化,如果類的時間戳變掉了,則將類重新載入。

熱加載與反射:

對比反射機制,反射是在運行時獲取類信息,通過動態的調用來改變程序行爲; 熱加載是在運行時通過重新加載改變類信息,直接改變程序行爲。

熱部署原理類似,但它是直接重新加載整個應用,這種方式會釋放內存,比熱加載更加乾淨徹底,但同時也更費時間。

 

原理:一個類加載器在運行期只能加載同一個類一次,無論該類是否被修改,所以需要新建類加載來加載同一個類,引用重定向了舊的加載器實例被拋棄。

 

JAVA熱部署(hotswap)實現

在不重啓 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行爲

Java 類是通過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載後,會生成對應的 Class 對象,之後就可創建該類的實例。

默認的虛擬機行爲只會在啓動時加載類,如果後期有一個類需要更新的話,單純替換編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。

 

如果要實現熱部署,有兩種方法:

1.修改虛擬機的源代碼,根本上改變 classloader 的加載行爲,使虛擬機能監聽 class 文件的更新,重新加載 class 文件,這樣的行爲破壞性很大,爲後續的 JVM 升級埋下了一個大坑。

2.創建自己的 classloader 來加載需要監聽的 class,這樣就能控制類加載的時機,從而實現熱部署。 

 

java類的加載過程:

一個java類文件到虛擬機裏的對象,要經過如下過程:首先通過java編譯器將java文件編譯成class字節碼,類加載器讀取class字節碼,再將類轉化爲實例,對實例newInstance就可生成對象。

類加載器ClassLoader功能,也就是將class字節碼轉換到類的實例。在java應用中,所有的實例都是由類加載器,加載而來。

https://images0.cnblogs.com/blog/531549/201411/030931301899477.png

一般在系統中,類的加載都是由系統自帶的類加載器完成,而且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被加載一次,而且無法被卸載。

希望將java類卸載,並且替換更新版本的java類,怎麼做?把類加載器換了,用自定義的替代。

 

實現熱部署步驟:

1、銷燬該自定義ClassLoader ,並重寫ClassLoader的findClass方法(被該加載器加載的class也會自動卸載)

2、更新class類文件

3、創建新的ClassLoader去加載loadClass更新後的class類文件。

 

外部顯示調用loadClass時。

1.此類之前加載過

1)期間Class重編譯過:新建加載器重加載(該ClassLoader有一個靜態引用給外部共享,熱替換時新建對象實例)

2)期間Class沒有重編譯過:返回之前的class

2.此類之前沒有加載過

核心類由系統加載器負責加載。用戶自定義類由原自定義加載器加載

 

替換的結果是返回一個class對象由外部反射調用

 

JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載(unload):

   - 該類所有的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
   - 加載該類的ClassLoader已經被GC。
   - 該類的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法

 

對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。

 

代碼實現

/** * 自定義類加載器,並override findClass方法 */

public class MyClassLoader extends ClassLoader{

     @Override

     public Class<?> findClass(String name) throws ClassNotFoundException{

            try{

                String fileName = name.substring(name.lastIndexOf("." )+1) + ".class" ;

                InputStream is = this.getClass().getResourceAsStream(fileName);

                 byte[] b = new byte[is.available()];

                is.read(b);

                 return defineClass(name, b, 0, b. length);

           } catch(IOException e){

                 throw new ClassNotFoundException(name);

           }

     }

}

package com.csair.soc.hotswap;

public class HelloWorld {

     public void say(){

           System. out.println( "Hello World V1");

     }

}

public class HelloWorld {

      public void say(){

           System. out.println( "Hello World V2");

     }

}

public class Hotswap {

     public static void main(String[] args) throws Exception {

            loadHelloWorld();

            // 回收資源,釋放HelloWorld.class文件,使之可被替換

           System. gc();

           Thread. sleep(1000);// 等待資源被回收

           File fileV2 = new File( "HelloWorld.class");

           File fileV1 = new File("bin\\com\\csair\\soc\\hotswap\\HelloWorld.class" );

           fileV1.delete(); //刪除V1版本

           fileV2.renameTo(fileV1); //更新V2版本

           System. out.println( "Update success!");

            loadHelloWorld();

     }

     public static void loadHelloWorld() throws Exception {

           MyClassLoader myLoader = new MyClassLoader(); //自定義類加載器

           Class<?> class1 = myLoader.findClass( "com.csair.soc.hotswap.HelloWorld");//類實例

           Object obj1 = class1.newInstance(); //生成新的對象

           Method method = class1.getMethod( "say");

           method.invoke(obj1); //執行方法say

           System. out.println(obj1.getClass()); //對象

           System. out.println(obj1.getClass().getClassLoader()); //對象的類加載器

     }

}

輸出結果:

Hello World V1

class com.csair.soc.hotswap.HelloWorld

com.csair.soc.hotswap.MyClassLoader@bfc8e0

Update success!

Hello World V2

class com.csair.soc.hotswap.HelloWorld

com.csair.soc.hotswap.MyClassLoader@860d49

 

根據結果可看到,在沒有重啓應用的情況下,成功的更新了HelloWorld類。

以上只是熱部署的最簡單的原理實踐,實際情況會複雜的多。

OSGI的最關鍵理念就是應用模塊(bundle)化,對於每一個bundle,都有其自己的類加載器,當要更新bundle時,把bundle和它的類加載器一起替換掉,就可實現模塊的熱替換

 

 

Tomcat類加載機制:

參考地址:https://www.cnblogs.com/aspirant/p/8991830.html

tomcat中都是通過擴展URLClassLoader來實現自己的類加載器

對於JVM來說:

按這個過程,如果同樣在CLASSPATH指定的目錄中和自己工作目錄中存放相同的class,會優先加載CLASSPATH目錄中的文件。

 

雙親委派模式優點:

避免類加載混亂,將類分層次,如javalang包下的類在jvm啓動時就被啓動類加載器加載了,而用戶一些代碼類則由應用程序類加載器(AppClassLoader)加載,基於雙親委託模式,就算用戶定義了與lang包中一樣的類,最終還是由應用程序類加載器委託給啓動類加載器去加載,這個時候啓動類加載器發現已經加載過了lang包下的類了,所以兩者都不會再重新加載。如果使用者通過自定義的類加載器可以強行打破這種雙親委託模型,但也不會成功的,java安全管理器拋出將會拋出java.lang.SecurityException異常

https://images0.cnblogs.com/blog2015/449064/201506/141327112691951.jpg

 

1、Tomcat 不遵循雙親委派機制,如果自定義一個惡意的HashMap,是否有風險?(阿里)

不會有風險,如果有,Tomcat都運行這麼多年了,那羣Tomcat大神能不改進嗎?

tomcat不遵循雙親委派機制,只是自定義的classLoader順序不同,但頂層還是相同的,還是要去頂層請求classloader.

 

Tomcat作爲web容器, 要解決什麼問題: 
1. 一個web容器可能要部署兩個應用程序,不同的應用程序可能會依賴同一個第三方類庫不同版本,不能要求同一個類庫在同一個服務器只有一份,因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。 
2. 部署在同一個web容器中相同的類庫相同的版本可共享。否則,如果服務器有10個應用程序,有10份相同的類庫加載進虛擬機。 
3. web容器也有自己依賴的類庫,不能與應用程序的類庫混淆。基於安全考慮,應讓容器類庫和程序類庫隔離開來。 
4. web容器要支持jsp的修改,jsp 文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行後修改jsp已經是司空見慣的事情,否則要你何用? 所以,web容器需要支持 jsp 修改後不用重啓(熱加載)。

總結:共享類庫、不同Web應用資源隔離、熱加載。

 

Tomcat 如果使用默認的類加載機制行不行? 

不行。

第一個問題,如果用默認類加載器機制,無法加載兩個相同類庫的不同版本,默認的累加器不管是什麼版本,只在乎全限定類名,並且只有一份。

第二個問題,默認的類加載器是能夠實現的,因爲他的職責就是保證唯一性。

第三個問題和第一個問題一樣。

第四個問題,jsp 文件其實也就是class文件,修改了,類名還是一樣,類加載器會直接取方法區中已經存在的,修改後的jsp不會重新加載。解決方案是直接卸載掉這jsp文件的類加載器,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。

 

Tomcat 如何實現自己獨特的類加載機制?設計圖:

https://images2018.cnblogs.com/blog/137084/201805/137084-20180526104342525-959933190.pnghttps://images0.cnblogs.com/blog2015/449064/201506/141304597074685.jpg

前3個類加載和默認的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader是Tomcat自己定義的類加載器,分別加載/common/*、/server/*、/shared/*(在tomcat 6之後已經合併到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebApp類加載器,每一個JSP文件對應一個Jsp類加載器。

•common Loader:Tomcat最基本的類加載器,加載路徑中的class可被Tomcat容器本身以及各個Webapp訪問;

•catalina Loader:Tomcat容器私有的類加載器,加載路徑中的class對於Webapp不可見;

•shared Loader:各個Webapp共享的類加載器,加載路徑中的class對於所有Webapp可見,但對於Tomcat容器不可見;

•Webapp ClassLoader:各個Webapp私有的類加載器,加載路徑中的class只對當前Webapp可見;

 

從圖中的委派關係中可以看出:

CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,實現了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方相互隔離。

WebAppClassLoader可用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。

JasperLoader的加載範圍僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並通過再建立一個新的Jsp類加載器來實現JSP文件的HotSwap功能。

 

 

Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,只是它們的類加載路徑不一樣,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader繼承自WebAppClassLoaderBase,基本所有邏輯都在WebAppClassLoaderBase爲中實現了, tomcat的所有類加載器都是以URLClassLoader爲基礎進行擴展

 

Common,Catalina,Shared類加載器是URLClassLoader類的一個實例,在默認的配置中,它們其實都是同一個對象,即commonLoader,結合初始化時的代碼(只保留關鍵代碼):

private void initClassLoaders() {

        commonLoader = createClassLoader("common", null);  // commonLoader的加載路徑爲common.loader

        if( commonLoader == null ) {

            commonLoader=this.getClass().getClassLoader();

        }

        catalinaLoader = createClassLoader("server", commonLoader); // 加載路徑爲server.loader,默認爲空,父類加載器爲commonLoader

        sharedLoader = createClassLoader("shared", commonLoader); // 加載路徑爲shared.loader,默認爲空,父類加載器爲commonLoader

    }

 private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {

        String value = CatalinaProperties.getProperty(name + ".loader");

        if ((value == null) || (value.equals("")))

            return parent; // catalinaLoader與sharedLoader的加載路徑均爲空,所以直接返回commonLoader對象,默認3者爲同一個對象

    }

在上面的代碼初始化時很明確是指出了,catalina與shared類加載器的父類加載器爲common類加載器,而初始化commonClassLoader時父類加載器設置爲null,最終會調到createClassLoader靜態方法:

public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent)  throws Exception {

        .....

        return AccessController.doPrivileged(

                new PrivilegedAction<URLClassLoader>() {

                    @Override

                    public URLClassLoader run() {

                        if (parent == null)

                            return new URLClassLoader(array);  //該構造方法默認獲取系統類加載器爲父類加載器,即AppClassLoader

                        else

                            return new URLClassLoader(array, parent);

                    }

                });

    }

在createClassLoader中指定參數parent==null時,最終會以系統類加載器(AppClassLoader)作爲父類加載器,這解釋了爲什麼commonClassLoader的父類加載器是AppClassLoader.

一個web應用對應着一個StandardContext實例,每個web應用都擁有獨立web應用類加載器(WebClassLoader),這個類加載器在StandardContext.startInternal()中被構造了出來:

 if (getLoader() == null) {

            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());

            webappLoader.setDelegate(getDelegate());

            setLoader(webappLoader);

        }

getParentClassLoader()會獲取父容器StandarHost.parentClassLoader對象屬性,而這個對象屬性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其實就是Catalina.parentClassLoader對象屬性,再來跟蹤一下Catalina.parentClassLoader,在Bootstrap.init()時通過反射調用了Catalina.setParentClassLoader(),將Bootstrap.sharedLoader屬性設置爲Catalina.parentClassLoader,所以WebClassLoader的父類加載器是Shared ClassLoader.

 

Q:tomcat 違背了java 推薦的雙親委派模型。

雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當由自己的父類加載器先加載。

Tomcat爲了實現隔離性,沒有遵守這個約定,每個webappClassLoader加載自己的目錄下的class文件,不會傳遞給父類加載器。

 

Tomcat的類加載機制違反了雙親委託原則的,對於一些未加載的非基礎類(Object,String),各個web應用自己的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給commonClassLoader走雙親委託。 具體加載邏輯位於WebAppClassLoaderBase.loadClass()方法中,過程大致如下:

1.先在本地緩存中查找是否已經加載過該類(對於一些已經加載了的類,會被緩存在resourceEntries這個數據結構中),如果已經加載即返回,否則 繼續下一步。

2.讓系統類加載器(AppClassLoader)嘗試加載該類,主要是爲了防止一些基礎類會被web中的類覆蓋,如果加載到即返回,返回繼續。

3.前兩步均沒加載到目標類,那麼web應用的類加載器將自行加載,如果加載到則返回,否則繼續下一步。

4.最後還是加載不到的話,則委託父類加載器(Common ClassLoader)去加載。

3.4違背了雙親委派機制。

除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一樣是違反了雙親委託

 

Q:Tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,怎麼辦?

可使用線程上下文類加載器實現,使用線程上下文加載器,可以讓父類加載器請求子類加載器去完成類加載的動作。

 

 

Tomcat的類加載器:

tomcat啓動時,會創建幾種類加載器:

1 Bootstrap 引導類加載器 :加載JVM啓動所需的類,以及標準擴展類(位於jre/lib/ext下)

2 System系統類加載器:加載tomcat啓動的類,如bootstrap.jar,通常在catalina.bat或catalina.sh中指定。CATALINA_HOME/bin下

3 Common 通用類加載器 :加載tomcat使用以及應用通用的一些類,位於CATALINA_HOME/lib下,如servlet-api.jar

4 webapp 應用類加載器:每個應用在部署後,都會創建一個唯一的類加載器。該類加載器會加載位於 WEB-INF/lib下的jar文件中的class WEB-INF/classes下的class文件。

 

當應用需要到某個類時,則會按照下面的順序進行類加載

  1 使用bootstrap引導類加載器加載

  2 使用system系統類加載器加載

  3 使用應用類加載器在WEB-INF/classes中加載

  4 使用應用類加載器在WEB-INF/lib中加載

  5 使用common類加載器在CATALINA_HOME/lib中加載

 

 

 

 

 

基礎練習:

 

延伸出來問題進行分析:

public class SSClass{

    static{

        System.out.println("SSClass");

    }

}  

public class SuperClass extends SSClass{

    static{

        System.out.println("SuperClass init!");

    }

    public static int value = 123;

    public SuperClass(){

        System.out.println("init SuperClass");

    }

}

public class SubClass extends SuperClass{

    static{

        System.out.println("SubClass init");

    }

    static int a;

    public SubClass()  {

        System.out.println("init SubClass");

    }

}

public class NotInitialization{

    public static void main(String[] args){

        System.out.println(SubClass.value);

    }

}

運行結果:

SSClass

SuperClass init!

123

 

 

public class ConstClass{

    static{

        System.out.println("ConstClass init!");

    }

    public static  final String HELLOWORLD = "hello world";

}

public class NotInitialization{

    public static void main(String[] args){

        System.out.println(ConstClass.HELLOWORLD);  // 運行結果只有HelloWorld

    }

}

 

類加載小總結:

1, JVM會先去方法區中找有沒有相應類的.class存在。如果有,就直接使用;如果沒有,則把相關類的.class加載到方法區

2, 在.class加載到方法區時,會分爲兩部分加載:先加載非靜態內容,再加載靜態內容

3, 加載非靜態內容:把.class中的所有非靜態內容加載到方法區下的非靜態區域內

4, 加載靜態內容:

4.1、把.class中的所有靜態內容加載到方法區下的靜態區域內

4.2、靜態內容加載完成之後,對所有的靜態變量進行默認初始化

4.3、所有的靜態變量默認初始化完成之後,再進行顯式初始化

4.4、當靜態區域下的所有靜態變量顯式初始化完後,執行靜態代碼塊

5,當靜態區域下的靜態代碼塊,執行完之後,整個類的加載就完成了。

 

 

 

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