JVM—【03】認識Java類的生命週期、類加載的過程、雙親委派機制

1. 初識類加載

  • JVM把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。java

  • 在Java語言中,類型的加載、鏈接和初始化過程都是在程序運行期間完成的。程序員

    這種策略在類加載時稍微會增長一些性能開銷,可是提升了Java應用程序的靈活性。數據庫

    Java天生能夠動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特色實現的。數組


2. 類的生命週期

JVM 類的聲明週期

  • 類從加載到虛擬機內存開始到卸載出內存爲止的生命週期包括7個階段:緩存

    加載、驗證、準備、初始化和卸載這5個順序是固定的,類加載過程必須按照這種順序循序漸進地開始。安全

    解析階段在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定。bash

    這些階段一般都是相互交叉地混合式進行的,一般會在一個階段執行的過程當中調用、激活另外一個階段。網絡

  • 有且只有5種必須進行類初始化的狀況(主動引用):數據結構

    遇到newgetstaticputstaticinvokestatic這4個字節碼指令時,最多見的Java代碼是場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候、調用一個類的靜態方法的時候.多線程

    使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要觸發其初始化。

    當初始化一個類的時候,若是其父類尚未進行過初始化,則須要先觸發其父類的初始化。

    當虛擬機啓動時,用戶須要指定一個要執行的主類,虛擬機會先初始化這個主類(包含main方法的類)

    當使用jdk的動態語言支持時,若是一個java.lang.invoke.Methodhandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先進行初始化。

  • 被動引用:

    經過子類引用父類的靜態字段,不會致使子類初始化。對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類類引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。至因而否要觸發子類的加載和驗證,在JVM規範中並無明確規定,這點取決於虛擬機的具體實現.示例代碼以下:

    package io.ilss.main;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
    
        public static int value = 123;
    
    }
    
    
    package io.ilss.main;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init!");
        }
    }
    
    package io.ilss.main;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class NotInitialization {
        public static void main(String[] args) { 
            System.out.println(SubClass.value);
        }
    }
    
    複製代碼

    經過數組定義來引用類,不會觸發此類的初始化。而是觸發這個數組元素類對應的數組類的初始化。如一個io.ilss.Demo類則對應[io.ilss.Demo的一個類,對於用戶代碼來講這不是一個合法的類名稱,它是由虛擬機自動生成的、直接繼承與java.lang.Object的子類,建立動做由字節碼指令newarray觸發。[io.ilss.Demo這個類表明了io.ilss.Demo對應的一位數組,數組中應有的屬性和方法都實如今這個類中,Java的數組訪問相對於C/C++來講更安全,由於這個類封裝了數組元素的訪問(封裝在了數組訪問指令xaload、xastore中)。

    package io.ilss.main;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class NotInitialization {
        public static void main(String[] args) {
            SuperClass[] superClasses = new SuperClass[10];
        }
    }
    
    複製代碼

    常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量類的初始化。這裏有個須要說明的地方,若是是在本類如第一處使用調用,是會加載ConstClass這個類的,這裏說的是在非本類的中調用這個常量不會初始化ConstClass,這是由於Java在編譯階段經過常量傳播優化,已經將hello world的值存到了NotInitialization類的常量池中,NotInitialization對「常量HELLO_WORLD的引用」都變成了對自身常量池的引用,實際上NotInitialization中不會有任何ConstClass類的符號引用,這兩個類在編譯成Class以後就不存在任何聯繫了。

    package io.ilss.main;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class ConstClass {
        static {
            System.out.println("ConstClass init");
        }
    
        public static final String HELLO_WORLD = "hello world";
        
        public static void main(String[] args) { // 1
            System.out.println(HELLO_WORLD);
        }
    }
    
    
    package io.ilss.main;
    
    import static io.ilss.main.ConstClass.HELLO_WORLD;
    
    /** * @author yiren * @date 2019-08-20 **/
    public class NotInitialization {
        public static void main(String[] args) { // 2
            System.out.println(HELLO_WORLD);
        }
    }
    複製代碼

    接口和類的加載略有不一樣,接口也有初始化過程,接口中沒有static{}代碼塊,可是編譯器仍會爲接口生成<clinit>()類構造器,用於初始化接口中定義的成員變量。接口與類真正的區別的是有且僅有的類初始化場景的第三種:當一個類在初始化時,要求其父類所有都已初始化過了,可是在接口中,並不會要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候纔會初始化(如去引用接口中定義的常量)


3. 類的加載過程

  • 類的加載全過程:加載、驗證、準備、解析和初始化

3.1. 加載

  • 加載階段JVM須要完成如下三件事情:

    經過一個類的全限定名來獲取定義此類的二進制字節流。

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

    在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

  • 經過一個類的全限定名來獲取定義此類的二進制字節流,並無指明要從哪裏獲取、怎樣獲取。能夠從:

    zip包中讀取,這很常見,最終變成了如今的:JAR、EAR、WAR格式基礎

    從網絡中獲取,之前有個叫Applet(已過期)就是這樣作的

    運行時計算生成,見得最多的就是動態代理技術,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來爲特定的接口生成形式爲"*$Proxy"的代理類的二進制字節流。

    由其餘文件生成:如JSP應用,由JSP生成對應的Class類

    從數據庫中獲取,這種場景相對少。

  • 加載階段中獲取類的二進制字節流的動做是開發人員可控性最強的,加載階段既可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去控制字節流的獲取方式。(即重寫一個類加載器的loadClass()方法)

  • 對於數組,有不一樣,數組類自己不經過類加載器建立,他是由Java虛擬機直接建立的。可是數組類與類加載器仍然關係密切,由於數組的元素類型是由類加載器去建立,一個數組類建立過程遵循如下規則:

    若是數組的組件類型(Component Tyep, 指的是數組中去掉一個維度的類型)是引用類型,那就遞歸採用本節中定義的類加載過程去加載這個組件類型,數組將在加載該組件類型的類加載器的類名稱空間上被標識。

    若是數組的組件類型不是引用類型(如int[])JVM將會把數組標記爲與引導類加載器關聯

    數組類的可見性與它的組件類型的可見性一致,若是組件類型不是引導類型,那數組類的可見性默認爲public。

  • 加載階段完成後,虛擬機外部的二進制流就按照虛擬機所需的格式存儲在方法中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規範未規定此區域的具體數據結構。而後在內存中實例化一個java.lang.Class類的對象,這個對象將做爲程序訪問方法區中的這些類型數據的外部接口

    對於HotSpot而言,Class對象比較特殊,它雖然是對象,可是存在了方法區中

  • 加載階段與鏈接階段是交叉進行的,加載階段未完成可能鏈接階段已經開始了,可是這兩個階段的前後順序是固定的。


3.2. 驗證

  • 驗證階段大體上會完成4個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

  • 文件格式驗證: 主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內格式上符合描述一個Java類型信息的要求。這個階段的驗證是基於二進制字節流進行的,只有經過了額這個階段的驗證後,字節流纔會進入到內存的方法區中進行存儲,後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。

    是否以魔數0xCAFEBABE開頭

    主次版本號是否在當前虛擬機處理範圍以內

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

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

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

    Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息。

    .....

  • 元數據驗證: 對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求;主要目的是堆類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息。

    這個類是否有父類(除java.lang.Object以外全部類都應當有父類)

    這個類的父類是否繼承了不容許被繼承的類(被final修飾)

    若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法

    類的字段、方法是否與父類產生矛盾(如:覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)

    ......

  • 字節碼校驗: 經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證被叫眼淚的方法在運行時不會作出危害虛擬機安全的時間

    保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧放了一個int類型的數據,使用時卻按long類型來加載入本地變量表中,

    確保跳轉指令不會跳轉到方法體之外的字節碼指令上。

    保證方法體中的類型轉換是有效的。如:子類轉父類是安全的,可是把父類賦給子類,甚至是把對象賦給把毫無繼承關係的、絕不相干的數據類型,則是危險和不合法的。

  • 符號引用驗證: 這個校驗發生在符號引用轉換成直接引用的時候,在鏈接的第三個階段解析中發生。能夠看作是堆類自身之外的信息進行匹配性校驗。須要校驗如下內容:

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

    在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。

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

    ......

    符號引用的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證,
    就會拋出java.lang.IncompatiableClassChangeError異常的子類,
    如:IllegalAccessError、NoSuchFieldError、NoSuchMethodError
    複製代碼

3.3. 準備

  • 正式爲類變量分配內存以及設置類變量(不是實例變量)初始值的階段,這些變量所用到的內存都將分配在方法區。初始值一般狀況下是該類型的零值

    public static int value = 123;

    這裏的初始值並非值123,而是值int的默認值0,而把value賦值123的putstatic指令,是須要在類構造器<clinit>()方法中,因此賦值會在初始化階段纔會執行。

  • 一般狀況以外的特殊狀況: 若是類字段的字段屬性表中存在ConstantValue屬性,那麼在準備階段value就會被初始化爲ConstantValue如:

    public static final int value = 123; 注意final

    上面的代碼,編譯時javac會將value生成ConstantValue屬性,在準備階段就會根據ConstantValue的設置將value設置成123.


3.4. 解析

  • 解析階段是JVM將常量池內的符號引用替換爲直接引用的過程;符號引用在Class文件中以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等類型的常量出現。

    符號引用(Symbolic References): 符號引用以一組符號來描述所描述引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與內存佈局無關,引用的目標不必定加載到內存中。符號引用的字面量形式明肯定義在JVM規範的Class文件格式中。

    直接引用(Direct Reference): 直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和JVM內存佈局相關的,同一個符號引用在不一樣JVM中翻譯出來的直接引用通常會不一樣。若是有了直接引用,那引用的目標一定已經在內存中存在。

  • JVM規範沒有規定解析階段發生的時間,只要求在執行16個操做符號引用的字節碼以前,先對他們所使用的符號引用進行解析。因此JVM能夠根據須要選擇是在類被類加載器加載時仍是符號引用要被使用的時去解析。

  • 對同一個符號引用屢次解析是很常見的事情,除invokedynamic外,續集你能夠對第一次解析的結果進行緩存從而避免重複解析。

    在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態。

  • JVM須要保證在同一個實體中,一個符號引用以前已經被成功解析後,後續的引用解析請求就應當一直成功;一樣的若是第一次解析失敗,那麼其餘指令對這個符號的解析請求也應當收到相同的異常。

  • 對於invokedynamic上面規則不成立。

  • 解析動做主要針對:類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符 7類 符號引用進行。

    1. 類或接口的解析

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

    1. 若是C是非數組類型,那虛擬機將會把表明N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程當中,因爲元數據驗證、字節碼驗證的須要,又可能觸發其餘相關類的加載動做,例如加載這個類的父類或實現的接口。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。
    1. 若是C是一個數組類型,而且數組的元素類型爲對象,也就是N的描述符會是相似[Ljava/lang/Integer的形式,那將會按照第1點的規則加載數組元素類型。若是N的描述符如前面所假設的形式,須要加載的元素類型就是「java.lang.Integer」,接着由虛擬機生成一個表明此數組維度和元素的數組對象
    1. 若是上面的步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認D是否具有對C的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError異常。
    1. 字段解析

    解析一個未被解析過的字段符號引用,首先會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用。若是在解析過程當中出現了任何異常,都會致使字段符號引用解析的失敗。若是解析成功完成,那將這個字段所屬的類或接口用C表示,JVM規範要求按照以下步驟對C進行後續字段的搜索。

    1. 若是C自己就包含簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
    2. 不然,若是在C中實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它的父接口,若是接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
    3. 不然,若是C不是java.lang.Object的話,將會按照繼承關係從下往上遞歸搜索其父類,若是在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
    4. 不然,查找失敗,拋出java.lang.NoSuchFieldError異常。

    若是查找過程成功返回了引用,將會對這個字段進行權限驗證,若是發現不具有對字段的訪問權限,將拋出java.lang.Ille-galAccessError異常。

    1. 類方法解析

    先解析出類方法表的class_index項中索引的方法所屬的類或接口的符號引用,若是解析成功,用C表示這個類,接下來JVM將會按照以下步驟進行後續的類方法搜索。

    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異常。

    1. 接口方法解析

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

    1. 與類方法解析不一樣,若是在接口方法表中發現class_index中的索引C是個類而不是接口,那就直接拋出java.lang.Incom-patibleClassChangeError異常。
    2. 不然,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
    3. 不然,在接口C的父接口中遞歸查找,直到java.lang.Object類(查找範圍會包括Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
    4. 不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

    在接口中,全部方法默認就是public因此不存在訪問權限問題,所以接口方法的符號解析應當不會拋出java.lang.IllegalAccessError


3.5. 初始化

  • 類初始化階段是類加載過程的最後一步,前面的,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段才真正開始執行類中定義的Java代碼或者說字節碼。

  • 在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序定製的主觀計劃去初始化類變量和其餘資源,或者能夠從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法執行過程當中一些可能會影響程序運行行爲的特色和細節。

    <clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問

    public class Test{
        static {
            i = 0;
            System.out.print(i)
        }
        static int i = 1;
    }
    複製代碼

    <clinit>()方法與類的構造函數(或者說實例構造器<clinit>()方法)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object

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

    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(Strintg[] args) {
        System.out.println(Sub.b) // 結果爲2
    }
    複製代碼

    <clinit>()方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法

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

    虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化類,只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個進程阻塞。(須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出<clinit>()方法後,其餘線程喚醒以後不會再次進入<clinit>()方法同一個類加載器下,一個類型只會初始化一次)

    static class DeadLoopClass {
        static {
            // 若是不加上這個if語句,編譯器將提示「Initializer does not complete normally」並拒絕編譯
        if (true) {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while (true) {
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Runnable script = () -> {
                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
    複製代碼

4. 類加載器

  • 「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓程序本身決定如何獲取所需的類。實現這個動做的代碼模塊稱爲「類加載器」。
  • 類加載器在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩,成爲了java體系中的一塊重要的基石。

4.1 類與類加載器

  • 對於任意一個類,都須要由它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間,
  • 比較兩個類是否「相等」,是由同一個類加載器加載的前提下才有意義,不然即便兩個類是同一個class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那這兩個類就一定不相等。

    這裏的相等,包括表明類的Class對象的equalsisAssignableFromisInstance方法返回的結果。也包括instanceof作的所屬關係斷定狀況。


4.2. 雙親委派模型

  • 從JVM的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言(HotSpot)實現,是虛擬機自身的一部分。另一種就是其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且都繼承自抽象java.lang.ClassLoader

  • 從Java開發人員的角度來看,絕大部分Java程序都會使用如下3種系統提供的類加載器。

    1. 啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在<JAVA_HOME>\lib目錄中,而且是虛擬機識別的(名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,用戶編寫自定義類加載器時,須要把加載請求委派給引導類加載器,那就直接使用null代替便可。
    2. 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,他負責加載**<JAVA_HOME>\lib\ext目錄中的類庫**,或者java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
    3. 應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader實現。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱他爲系統類加載器。他負責加載用戶類路徑上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義本身的類加載器,通常狀況下這個就是程序中的默認類加載器
  • 雙親委派模型(Parents Delegation Model):雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。

  • 雙親委派模型的工做過程是:

    1. 若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
    2. 每個層次的類加載器都是如此。所以,全部的加載請求最終都應該傳送到頂層的啓動類加載器中。
    3. 只有當父加載器反饋本身沒法完成這個加載請求時(搜索範圍中沒有找到所需的類),子加載器纔會嘗試本身去加載。

    類加載 - 雙親委派模型

  • 使用雙親委派模型來組織類加載器之間的關係,好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。

    如類java.lang.Object,它存放在rt.jar之中,不管哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。相反,若是沒有使用雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身編寫一個稱謂java.lang.Object的類,並放在程序的ClassPath中,那系統中將會出現多個不一樣的Object類,Java類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂。

    雙親委派的實現代碼:

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 首先,檢查類是否已經加載
                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
                    }
    
                    if (c == null) {
                        // 若是仍然沒有找到該類,那麼調用findClass來找到該類。
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // 這是定義類裝入器;記錄數據
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    複製代碼

4.3. 破壞雙親委派

  • 雙親委託模型並非一個強制性的約束,而是Java設計者推薦給開發者的類加載器實現方式。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外,雙親委派模型主要出現過3個較大規模的「被破壞」的狀況。

  • 因爲雙親委派模型在JDK 1.2以後才被引入,爲了向前兼容,JDK 1.2以後的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此以前,用戶去繼承java.lang.ClassLoader的惟一目的就是爲了重寫loadClass()方法,由於虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的惟一邏輯就是去調用本身的loadClass()

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

  • 雙親委派很好的解決各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之因此稱爲「基礎」,是由於他們老是做爲被用戶代碼調用的API,但事實每每沒有絕對的完美,若是基礎類又要調用回用戶的代碼該怎麼解決。

    一個典型的例子即是JNDI服務,JNDI如今已是Java的標準服務,他的代碼由啓動類加載器去加載(在JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,他須要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,

    爲了解決這個問題,Java設計團隊引入了個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器能夠經過java.lang.Thread類的setContextClassLoaser()方法進行設置,若是建立線程時還未設置,他將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

    有了線程上下文類加載器,就能夠作一些「舞弊」的事情了,JNDI服務使用這個線程上下文類加載器去加載所須要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動做,這彙總行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的通常性原則,但這也是迫不得已的事情。Java中全部涉及SPI的加載動做基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等

  • 第三次「被破壞」是因爲用戶對程序動態性的追求而致使的,「動態性」指的是:代碼熱替換(HotSwap)、模塊熱部署(HotDeployment)等,但對於一些生產系統來講,關機重啓一次可能就要被列爲生產事故,這種狀況下熱部署就對軟件開發者,尤爲是企業級軟件開發者具備很大的吸引力。

    Sun公司所提出的JSR-29四、JSR-277規範在與JCP組織的模塊化規範之爭中落敗給JSR-291(即OSGI R4.2),目前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. 不然,類查找失敗。

關於我

  • 座標杭州,普通本科在讀,計算機科學與技術專業,20年畢業,目前處於實習階段。
  • 主要作Java開發,會寫點Golang、Shell。對微服務、大數據比較感興趣,預備作這個方向。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎你們和我交流鴨!!!
相關文章
相關標籤/搜索