【JAVA】深入理解虛擬機之虛擬機類加載機制

屏幕快照 2018-05-14 下午6.50.43.png

Java程序運行時,數據會分區存放,JavaStack(Java棧)、 heap(堆)、method(方法區)。

1、Java棧

Java棧的區域很小,只有1M,特點是存取速度很快,所以在stack中存放的都是快速執行的任務,基本數據類型的數據,和對象的引用(reference)。

駐留于常規RAM(隨機訪問存儲器)區域。但可通過它的“棧指針”獲取處理的直接支持。棧指針若向下移,會創建新的內存;若向上移,則會釋放那些內存。這是一種特別快、特別有效的數據保存方式,僅次于寄存器。創建程序時,Java編譯器必須準確地知道堆棧內保存的所有數據的“長度”以及“存在時間”。這是由于它必須生成相應的代碼,以便向上和向下移動指針。這一限制無疑影響了程序的靈活性,所以盡管有些Java數據要保存在棧里——特別是對象句柄,但Java對象并不放到其中。

JVM只會直接對JavaStack(Java棧)執行兩種操作:①以幀為單位的壓棧或出棧;②通過-Xss來設置, 若不夠會拋出StackOverflowError異常。

1.每個線程包含一個棧區,棧中只保存基本數據類型的數據和自定義對象的引用(不是對象),對象都存放在堆區中
2.每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問。
3.棧分為3個部分:基本數據類型的變量區、執行環境上下文、操作指令區(存放操作指令)。

棧是存放線程調用方法時存儲局部變量表,操作,方法出口等與方法執行相關的信息,Java棧所占內存的大小由Xss來調節,方法調用層次太多會撐爆這個區域。

2、程序計數器(ProgramCounter)寄存器

PC寄存器( PC register ):每個線程啟動的時候,都會創建一個PC(Program Counter,程序計數器)寄存器。PC寄存器里保存有當前正在執行的JVM指令的地址。 每一個線程都有它自己的PC寄存器,也是該線程啟動時創建的。保存下一條將要執行的指令地址的寄存器是 :PC寄存器。PC寄存器的內容總是指向下一條將被執行指令的地址,這里的地址可以是一個本地指針,也可以是在方法區中相對應于該方法起始指令的偏移量。

3、本地方法棧

Nativemethodstack(本地方法棧):保存native方法進入區域的地址。

4、堆

類的對象放在heap(堆)中,所有的類對象都是通過new方法創建,創建后,在stack(棧)會創建類對象的引用(內存地址)。

一種常規用途的內存池(也在RAM(隨機存取存儲器 )區域),其中保存了Java對象。和棧不同:“內存堆”或“堆”最吸引人的地方在于編譯器不必知道要從堆里分配多少存儲空間,也不必知道存儲的數據要在堆里停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。要求創建一個對象時,只需用new命令編輯相應的代碼即可。執行這些代碼時,會在堆里自動進行數據的保存。當然,為達到這種靈活性,必然會付出一定的代價:在堆里分配存儲空間時會花掉更長的時間。

JVM將所有對象的實例(即用new創建的對象)(對應于對象的引用(引用就是內存地址))的內存都分配在堆上,堆所占內存的大小由-Xmx指令和-Xms指令來調節,sample如下所示:

public class HeapOOM {              
    static class OOMObject{}          
    /**       
     * @param args       
     */       
    public static void main(String[] args) {           
        List list = new ArrayList();// List類和ArrayList類都是集合類,
                                    // 但是ArrayList可以理解為順序表,
                                    // 屬于線性表。                      
        while (true) {               
            list.add(new OOMObject());           
        }       
    }      
}

加上JVM參數-verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError,就能很快報出OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
并且能自動生成Dump。

5、方法區

method(方法區)又叫靜態區,存放所有的①類(class),②靜態變量(static變量),③靜態方法,④常量和⑤成員方法。

1.又叫靜態區,跟堆一樣,被所有的線程共享。

2.方法區中存放的都是在整個程序中永遠唯一的元素。這也是方法區被所有的線程共享的原因。

(順便展開靜態變量和常量的區別: 靜態變量本質是變量,是整個類所有對象共享的一個變量,其值一旦改變對這個類的所有對象都有影響;常量一旦賦值后不能修改其引用,其中基本數據類型的常量不能修改其值。)

Java里面是沒有靜態變量這個概念的,不信你自己在某個成員方法里面定義一個static int i = 0;Java里只有靜態成員變量。它屬于類的屬性。至于他放哪里?樓上說的是靜態區。我不知道到底有沒有這個翻譯。但是深入JVM里是翻譯為方法區的。虛擬機的體系結構:①Java棧,② 堆,③PC寄存器,④方法區,⑤本地方法棧,⑥運行常量池。而方法區保存的就是一個類的模板,堆是放類的實例(即對象)的。棧是一般來用來函數計算的。隨便找本計算機底層的書都知道了。棧里的數據,函數執行完就不會存儲了。這就是為什么局部變量每一次都是一樣的。就算給他加一后,下次執行函數的時候還是原來的樣子。

方法區的大小由-XX:PermSize和-XX:MaxPermSize來調節,類太多有可能撐爆永久代。靜態變量或常量也有可能撐爆方法區。

6、運行常量池

這兒的“靜態”是指“位于固定位置”。程序運行期間,靜態存儲的數據將隨時等候調用。可用static關鍵字指出一個對象的特定元素是靜態的。但Java對象本身永遠都不會置入靜態存儲空間。

這個區域屬于方法區。該區域存放類和接口的常量,除此之外,它還存放成員變量和成員方法的所有引用。當一個成員變量或者成員方法被引用的時候,JVM就通過運行常量池中的這些引用來查找成員變量和成員方法在內存中的的實際地址。

7、舉例分析

例子如下:
為了更清楚地搞明白程序運行時,數據區里的情況,我們來準備2個小道具(2個非常簡單的小程序)。

// AppMain.java
public class AppMain {                         //運行時,JVM把AppMain的信息都放入方法區    

    public static void main(String[] args) { //main成員方法本身放入方法區。    
        Sample test1 = new  Sample( " 測試1 " );   //test1是引用,所以放到棧區里,Sample是自定義對象應該放到堆里面    
        Sample test2 = new  Sample( " 測試2 " );         
        test1.printName();    
        test2.printName();    
    }
    
} 

// Sample.java       
public class Sample {   //運行時,JVM把appmain的信息都放入方法區。            

    private  name;      //new Sample實例后,name引用放入棧區里,name對象放入堆里。     

    public  Sample(String name) {    
        this .name = name;    
    }          
        
    public   void  printName() {// printName()成員方法本身放入方法區里。    
        System.out.println(name);    
    }    
}   

OK,讓我們開始行動吧,出發指令就是:“java AppMain”,包包里帶好我們的行動向導圖。

屏幕快照 2018-05-14 下午7.12.19.png

系統收到了我們發出的指令,啟動了一個Java虛擬機進程,這個進程首先從classpath中找到AppMain.class文件,讀取這個文件中的二進制數據,然后把Appmain類的類信息存放到運行時數據區的方法區中。這一過程稱為AppMain類的加載過程。

接著,JVM定位到方法區中AppMain類的Main()方法的字節碼,開始執行它的指令。這個main()方法的第一條語句就是:
Sample test1 = new Sample("測試1");
語句很簡單啦,就是讓JVM創建一個Sample實例,并且呢,使引用變量test1引用這個實例。貌似小case一樁哦,就讓我們來跟蹤一下JVM,看看它究竟是怎么來執行這個任務的:
1、Java虛擬機一看,不就是建立一個Sample類的實例嗎,簡單,于是就直奔方法區(方法區存放已經加載的類的相關信息,如類、靜態變量和常量)而去,先找到Sample類的類型信息再說。結果呢,嘿嘿,沒找到@@,這會兒的方法區里還沒有Sample類呢(即Sample類的類信息還沒有進入方法區中)。可JVM也不是一根筋的笨蛋,于是,它發揚“自己動手,豐衣足食”的作風,立馬加載了Sample類, 把Sample類的相關信息存放在了方法區中。

2、Sample類的相關信息加載完成后。Java虛擬機做的第一件事情就是在堆中為一個新的Sample類的實例分配內存,這個Sample類的實例持有著指向方法區的Sample類的類型信息的引用(Java中引用就是內存地址)。這里所說的引用,實際上指的是Sample類的類型信息在方法區中的內存地址,其實,就是有點類似于C語言里的指針啦~~,而這個地址呢,就存放了在Sample類的實例的數據區中。

3、在JVM中的一個進程中,每個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程,棧中的每一個元素被稱為棧幀,每當線程調用一個方法的時候就會向方法棧中壓入一個新棧幀。這里的幀用來存儲方法的參數、局部變量和運算過程中的臨時數據。OK,原理講完了,就讓我們來繼續我們的跟蹤行動!位于“=”前的test1是一個在main()方法中定義的變量,可見,它是一個局部變量,因此,test1這個局部變量會被JVM添加到執行main()方法的主線程的Java方法調用棧中。而“=”將把這個test1變量指向堆區中的Sample實例,也就是說,test1這個局部變量持有指向Sample類的實例的引用(即內存地址)。

OK,到這里為止呢,JVM就完成了這個簡單語句的執行任務。參考我們的行動向導圖,我們終于初步摸清了JVM的一點點底細了,COOL!

接下來,JVM將繼續執行后續指令,在堆區里繼續創建另一個Sample類的實例,然后依次執行它們的printName()方法。當JVM執行test1.printName()方法時,JVM根據局部變量test1持有的引用,定位到堆中的Sample類的實例,再根據Sample類的實例持有的引用,定位到方法區中Sample類的類型信息(包括①類,②靜態變量,③靜態方法,④常量和⑤成員方法),從而獲取printName()成員方法的字節碼,接著執行printName()成員方法包含的指令。

虛擬機棧

棧區:棧中分配的是基本類型和自定義對象的引用。
每個線程包含一個棧區,棧中只保存基礎數據類型和自定義對象的引用(不是對象),對象都存放在堆區中
每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問。
棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。

棧是存放線程調用方法時存儲局部變量表,操作,方法出口等與方法執行相關的信息,棧大小由Xss來調節,方法調用層次太多會撐爆這個區域。

棧溢出一般只會出現無限循環的遞歸中,另外,線程太多也會占滿棧區域

棧幀: 一個完整的棧幀包含:局部變量表(基本數據類型變量),操作數棧,動態連接信息,方法完成和異常完成信息。

局部變量表概念和特征:
由若干個Slot組成,長度由編譯期決定。
單個Slot可以存儲一個類型為boolean ,byte,char, short, float, reference和returnAddress的數據,兩個Slot可以存儲一個類型為long或double的數據。
局部變量表用于方法間參數傳遞,以及方法執行過程中存儲基礎數據類型的值和對象的引用。

本地方法棧:

本地方法棧的特征:
線程私有
后進先出棧
作用是支撐Native方法的調用,執行和退出
可能出現OutOfMemoryError異常和StackOverflowError異常

java虛擬機棧和本地方法棧可能發生如下異常情況:
如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量時,Java虛擬機將會拋出一個StackOverflowError異常。

如果Java虛擬機可以動態擴展,并且擴展的動作已經嘗試過,但是目前無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常。

2)方法區
方法區是存放虛擬機加載類的相關信息,如類、靜態變量和常量,大小由-XX:PermSize和-XX:MaxPermSize來調節,類太多有可能撐爆永久帶:

public class MethodAreaOOM {  
      
    static class OOMOjbect{}  
  
    /** 
     * @param args 
     */  
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        while(true){  
            Enhancer eh = new Enhancer();  
            eh.setSuperclass(OOMOjbect.class);  
            eh.setUseCache(false);  
            eh.setCallback(new MethodInterceptor(){  
                @Override  
                public Object intercept(Object arg0, Method arg1,  
                        Object[] arg2, MethodProxy arg3) throws Throwable {  
                    // TODO Auto-generated method stub  
                    return arg3.invokeSuper(arg0, arg2);  
                }  
            });  
            eh.create();  
        }  
    }  
}

加上永久帶的JVM參數:-XX:PermSize=10M -XX:MaxPermSize=10M,運行后會報如下異常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

靜態變量或常量也會有可能撐爆方法區:

public class ConstantOOM {  
  
    /** 
     * @param args 
     */  
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        List<String> list = new ArrayList<String>();  
        int i=0;  
        while(true){  
            list.add(String.valueOf(i++).intern());  
        }  
    }  
}  

3)Java棧和本地方法棧

簡單說說類加載過程,里面執行了哪些操作?
對類加載器有了解嗎?
什么是雙親委派模型?
雙親委派模型的工作過程以及使用它的好處。

虛擬機類加載機制的概念
虛擬機把描述類的數據從class文件加載到內存,并對數據進行校驗、轉換解析和初始化。最終形成可以被虛擬機最直接使用的java類型的過程就是虛擬機的類加載機制。

Java語言的動態加載和動態連接
另外需要注意的很重要的一點是:java語言中類型的加載連接以及初始化過程都是在程序運行期間完成的,這種策略雖然會使類加載時稍微增加一些性能開銷,但是會為java應用程序提供高度的靈活性。java里天生就可以動態擴展語言特性就是依賴運行期間動態加載和動態連接這個特點實現的。比如,如果編寫一個面向接口的程序,可以等到運行時再指定其具體實現類。

類加載時機

類從被加載到虛擬機內存到卸出內存為止,它的整個生命周期包括:


屏幕快照 2018-05-05 下午7.35.28.png

虛擬機規范嚴格規定了有且只有五種情況必須立即對類進行“初始化”:
使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段的時候,已經調用一個類的靜態方法的時候。
使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有初始化,則需要先觸發其初始化。
當初始化一個類的時候,如果發現其父類沒有被初始化就會先初始化它的父類。
當虛擬機啟動的時候,用戶需要指定一個要執行的主類(就是包含main()方法的那個類),虛擬機會先初始化這個類;
使用Jdk1.7動態語言支持的時候的一些情況。

而對于接口,當一個接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口時(如引用父接口中定義的常量)才會初始化。

所有引用類的方式都不會觸發初始化稱為被動引用,下面是3個被動引用例子:
①通過子類引用父類靜態字段,不會導致子類初始化;②通過數組定義引用類,不會觸發此類的初始化

public class SuperClass {
    static {
        System.out.println("SuperClass(父類)被初始化了。。。");
    }
    public static int value = 66;
}
public class Subclass extends SuperClass {
    static {
        System.out.println("Subclass(子類)被初始化了。。。");
    }
}
public class Test1 {
    public static void main(String[] args) {
        // 1:通過子類調用父類的靜態字段不會導致子類初始化
        // System.out.println(Subclass.value);//SuperClass(父類)被初始化了。。。66
        // 2:通過數組定義引用類,不會觸發此類的初始化
        SuperClass[] superClasses = new SuperClass[3];
        // 3:通過new 創建對象,可以實現類初始化,必須把1下面的代碼注釋掉才有效果不然經過1的時候類已經初始化了,下面這條語句也就沒用了。
        //SuperClass superClass = new SuperClass();
    }
}

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

public class ConstClass {
    static {
        System.out.println("ConstClass被初始化了。。。");
    }
    public static final String HELLO = "hello world";
}
public class Test2 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);//輸出結果:hello world
    }
}

類加載過程

下面我們詳細的說一下java虛擬機中類加載的全過程:加載、驗證、準備、解析和初始化這5個階段鎖執行的具體工作。

1.加載,“加載” 是 “類加載” 過程的一個階段,切不可將二者混淆。
加載階段由三個基本動作組成:
通過類型的完全限定名,產生一個代表該類型的二進制數據流(根本沒有指明從哪里獲取、怎樣獲取,可以說一個非常開放的平臺了)
解析這個二進制數據流為方法區內的運行時數據結構
創建一個表示該類型的java.lang.Class類的實例,作為方法區這個類的各種數據的訪問入口。

通過類型的完全限定名,產生一個代表該類型的二進制數據流的幾種常見形式:
從zip包中讀取,成為日后JAR、EAR、WAR格式的基礎;
從網絡中獲取,這種場景最典型的應用就是Applet;
運行時計算生成,這種場景最常用的就是動態代理技術了;
由其他文件生成,比如我們的JSP;

注意: 非數組類加載階段既可以使用系統提供的類加載器來完成,也可以由用戶自定義的類加載器去完成。(即重寫一個類加載器的loadClass()方法)

2.驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
虛擬機如果不檢查輸入的字節流,并對其完全信任的話,很可能會因為載入了有害的字節流而導致系統崩潰,所以驗證是虛擬機對自身保護的一項重要工作。這個階段是否嚴謹,直接決定了java虛擬機是否能承受惡意代碼的攻擊。
從整體上看,驗證階段大致上會完成4個階段的校驗工作:文件格式、元數據、字節碼、符號引用。
2.1文件格式驗證
驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。該驗證階段的主要目的是保證輸入的字節流能正確地解析并存儲于方法區之內。這個階段驗證是基于二進制字節流進行的,只有通過這個階段的驗證后,字節流才會進入內存的方法區進行存儲,所以后面的3個階段的全部是基于方法區的存儲結構進行的,不會再直接操作字節流。
2.2元數據驗證
該階段對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規范的要求,目的是保證不存在不符合Java語言規范的元數據信息。
2.3字節碼驗證
該階段主要工作時進行數據流和控制流分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。 例如,保證跳轉指令不會跳轉到方法體以外的字節碼指令上、保證方法體中的類型轉換是有效的等等。
由于數據流校驗的高復雜性,耗時較大,所以JDK1.6之后,在Javac中引入一項優化方法(可以通過參數關閉):在方法體的Code屬性的屬性表中增加一項“StackMapTable”屬性,該屬性描述了方法體中所有基本塊開始時本地變量表和操作棧應有的狀態,從而將字節碼驗證的類型推導轉變為類型檢查從而節省一些時間。
注意: 如果一個方法體通過了字節碼驗證,也不能說明其一定是安全的,因為校驗程序邏輯無法做到絕對精確。
2.4符號引用驗證
最后一個階段的校驗發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三個階段——解析階段中發生。符號引用驗證的目的是確保解析動作能正常執行。
驗證的內容主要有:
符號引用中通過字符串描述的全限定名是否能找到對應的類;
在指定類中是否存在符號方法的字段描述及簡單名稱所描述的方法和字段;
符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。

3.準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。(備注:這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中)。
初始值通常是數據類型的零值:
對于:public static int value = 123;,那么變量value在準備階段過后的初始值為0而不是123,這時候尚未開始執行任何java方法,把value賦值為123的動作將在初始化階段才會被執行。
一些特殊情況:
對于:public static final int value = 123;編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為123。

  1. 解析
    解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
    那么符號引用與直接引用有什么關聯呢?
    4.1 看兩者的概念。
    符號引用(Symbolic References): 符號引用以一組符號來描述所引用的目標,符號可以是符合約定的任何形式的字面量,符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中。
    直接引用(Direct References): 直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用與虛擬機實現的內存布局相關,引用的目標必定已經在內存中存在。
    虛擬機規范沒有規定解析階段發生的具體時間,虛擬機實現可以根據需要來判斷到底是在類被加載時解析還是等到一個符號引用將要被使用前才去解析。
    4.2 對解析結果進行緩存
    同一符號引用進行多次解析請求是很常見的,除invokedynamic指令以外,虛擬機實現可以對第一次解析結果進行緩存,來避免解析動作重復進行。無論是否真正執行了多次解析動作,虛擬機需要保證的是在同一個實體中,如果一個引用符號之前已經被成功解析過,那么后續的引用解析請求就應當一直成功;同樣的,如果 第一次解析失敗,那么其他指令對這個符號的解析請求也應該收到相同的異常。
    4.3 解析動作的目標
    解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。前面四種引用的解析過程,對于后面三種,與JDK1.7新增的動態語言支持息息相關,由于java語言是一門靜態類型語言,因此沒有介紹invokedynamic指令的語義之前,沒有辦法將他們和現在的java語言對應上。

  2. 初始化
    類初始化階段是類加載的最后一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的java程序代碼(或者說是字節碼)。

類加載器

1.類與類加載器
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。如果兩個類來源于同一個Class文件,只要加載它們的類加載器不同,那么這兩個類就必定不相等。

2.類加載器介紹
從Java虛擬機的角度分為兩種不同的類加載器:啟動類加載器(Bootstrap ClassLoader) 和其他類加載器。其中啟動類加載器,使用C++語言實現,是虛擬機自身的一部分;其余的類加載器都由Java語言實現,獨立于虛擬機之外,并且全都繼承自java.lang.ClassLoader類。(這里只限于HotSpot虛擬機)。
從Java開發人員的角度來看,絕大部分Java程序都會使用到以下3種系統提供的類加載器。
啟動類加載器(Bootstrap ClassLoader):
這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
擴展類加載器(Extension ClassLoader):
這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader):
這個類加載器由sun.misc.Launcher$AppClassLoader實現。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
我們的應用程序都是由這3種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。

3.雙親委派模型
雙親委派模型(Pattern Delegation Model),要求除了頂層的啟動類加載器外,其余的類加載器都應該有自己的父類加載器。這里父子關系通常是子類通過組合關系而不是繼承關系來復用父加載器的代碼。


屏幕快照 2018-05-05 下午8.02.32.png

雙親委派模型的工作過程: 如果一個類加載器收到了類加載的請求,先把這個請求委派給父類加載器去完成(所以所有的加載請求最終都應該傳送到頂層的啟動類加載器中),只有當父加載器反饋自己無法完成加載請求時,子加載器才會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。
注意:雙親委派模型是Java設計者們推薦給開發者們的一種類加載器實現方式,并不是一個強制性 的約束模型。在java的世界中大部分的類加載器都遵循這個模型,但也有例外。

4.破壞雙親委派模型
雙親委派模型主要出現過3次較大規模“被破壞”的情況。

第一次破壞是因為類加載器和抽象類java.lang.ClassLoader在JDK1.0就存在的,而雙親委派模型在JDK1.2之后才被引入,為了兼容已經存在的用戶自定義類加載器,引入雙親委派模型時做了一定的妥協:在java.lang.ClassLoader中引入了一個findClass()方法,在此之前,用戶去繼承java.lang.Classloader的唯一目的就是重寫loadClass()方法。JDK1.2之后不提倡用戶去覆蓋loadClass()方法,而是把自己的類加載邏輯寫到findClass()方法中,如果loadClass()方法中如果父類加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派模型規則的。
第二次破壞是因為模型自身的缺陷,現實中存在這樣的場景:基礎的類加載器需要求調用用戶的代碼,而基礎的類加載器可能不認識用戶的代碼。為此,Java設計團隊引入的設計時“線程上下文類加載器(Thread Context ClassLoader)”。這樣可以通過父類加載器請求子類加載器去完成類加載動作。已經違背了雙親委派模型的一般性原則。
第三次破壞 是由于用戶對程序動態性的追求導致的。這里所說的動態性是指:“代碼熱替換”、“模塊熱部署”等等比較熱門的詞。說白了就是希望應用程序能夠像我們的計算機外設一樣,接上鼠標、U盤不用重啟機器就能立即使用。OSGi是當前業界“事實上”的Java模塊化標準,OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現。每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容