天天看點

JVM類加載機制

1 什麼是類的加載

  類的加載是将類的.class檔案中的二進制資料讀入到記憶體中,将其放在運作時資料區的方法區内(将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構),然後再堆區建立一個java.lang.Class對象,用來封裝類在方法區内的資料結構,并且向java程式員提供了通路方法區内的資料結構的接口。

JVM類加載機制

類加載器并不需要等到某個類被“首次主動使用”時再加載它,JVM規範允許類加載器在預料某個類将要被使用時就預先加載它,如果在預先加載的過程中遇到了.class檔案缺失或存在錯誤,類加載器必須在程式首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程式主動使用,那麼類加載器就不會報告錯誤。

加載.class檔案的方式
– 從本地系統中直接加載
– 通過網絡下載下傳.class檔案
– 從zip,jar等歸檔檔案中加載.class檔案
– 從專有資料庫中提取.class檔案
– 将Java源檔案動态編譯為.class檔案      

2 類的生命周期

生命周期 :  裝載 - 連結 - 初始化 - 使用 - 解除安裝

JVM類加載機制

其中類加載的過程包括了裝載、驗證、準備、解析、初始化五個階段。在這五個階段中,裝載、驗證、準備和初始化這四個階段發生的順序是确定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的運作時綁定(也成為動态綁定或晚期綁定)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

2.1 裝載:

裝載時類加載過程的第一個階段,在裝載階段,虛拟機需要完成以下三件事情:

    1、通過一個類的全限定名來擷取其定義的二進制位元組流。

    2、将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。

    3、在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些資料的通路入口。

    相對于類加載的其他階段而言,裝載階段(準确地說,是裝載階段擷取類的二進制位元組流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成裝載,也可以自定義自己的類加載器來完成裝載。

   裝載階段完成後,虛拟機外部的 二進制位元組流就按照虛拟機所需的格式存儲在方法區之中,而且在Java堆中也建立一個java.lang.Class類的對象,這樣便可以通過該對象通路方法區中的這些資料。

2.2 連結

 – 驗證:確定被加載的類的正确性

驗證是連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。驗證階段大緻會完成4個階段的檢驗動作:

檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以0xCAFEBABE開頭、主次版本号是否在目前虛拟機的處理範圍之内、常量池中的常量是否有不被支援的類型。

中繼資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。

位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。

符号引用驗證:確定解析動作能正确執行。

驗證階段是非常重要的,但不是必須的,它對程式運作期沒有影響,如果所引用的類經過反複驗證,那麼可以考慮采用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛拟機類加載的時間。

 – 準備:為類的靜态變量配置設定記憶體,并将其初始化為預設值

   準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些記憶體都将在方法區中配置設定。對于該階段有以下幾點需要注意:

    1、這時候進行記憶體配置設定的僅包括類變量(static),而不包括執行個體變量,執行個體變量會在對象執行個體化時随着對象一塊配置設定在Java堆中。

    2、這裡所設定的初始值通常情況下是資料類型預設的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

   假設一個類變量的定義為:public static int value = 3;

   那麼變量value在準備階段過後的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value指派為3的putstatic指令是在程式編譯後,存放于類構造器<clinit>()方法之中的,是以把value指派為3的動作将在初始化階段才會執行。

    3、如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化為ConstValue屬性所指定的值。

   假設上面的類變量value被定義為: public static final int value = 3;

   編譯時Javac将會為value生成ConstantValue屬性,在準備階段虛拟機就會根據ConstantValue的設定将value指派為3。回憶上一篇博文中對象被動引用的第2個例子,便是這種情況。我們可以了解為static final常量在編譯期就将其結果放入了調用它的類的常量池中

這裡還需要注意如下幾點:
· 對基本資料類型來說,對于類變量(static)和全局變量,如果不顯式地對其指派而直接使用,則系統會為其賦予預設的零值,而對于局部變量來說,在使用前必須顯式地為其指派,否則編譯時不通過。
· 對于同時被static和final修飾的常量,必須在聲明的時候就為其顯式地指派,否則編譯時不通過;而隻被final修飾的常量則既可以在聲明時顯式地為其指派,也可以在類初始化時顯式地為其指派,總之,在使用前必須為其顯式地指派,系統不會為其賦予預設零值。
· 對于引用資料類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地指派而直接使用,系統都會為其賦予預設的零值,即null。
· 如果在數組初始化時沒有對數組中的各元素指派,那麼其中的元素将根據對應的資料類型而被賦予預設的零值。      

– 解析:把類中的符号引用轉換為直接引用

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

符号引用就是一組符号來描述目标,可以是任何字面量。

直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。

2.3 初始化

初始化,對類的靜态變量,靜态代碼塊執行初始化工作。(按照static在代碼中的順序順序執行)

在Java中對類變量進行初始值設定有兩種方式:

  ①聲明類變量是指定初始值

  ②使用靜态代碼塊為類變量指定初始值

 JVM初始化步驟

 1、假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類

 2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類

 3、假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:隻有當對類的主動使用的時候才會導緻類的初始化,類的主動使用包括以下六種:

– 建立類的執行個體,也就是new的方式

– 通路某個類或接口的靜态變量,或者對該靜态變量指派

– 調用類的靜态方法

– 反射(如Class.forName(“com.shengsiyuan.Test”))

– 初始化某個類的子類,則其父類也會被初始化

– Java虛拟機啟動時被标明為啟動類的類(Java Test),直接使用java.exe指令來運作某個主類

結束生命周期

•在如下幾種情況下,Java虛拟機将結束生命周期

– 執行了System.exit()方法

– 程式正常執行結束

– 程式在執行過程中遇到了異常或錯誤而異常終止

– 由于作業系統出現錯誤而導緻Java虛拟機程序終止

3 類加載器

JVM類加載機制

類加載器雙親委派模型

注意:這裡父類加載器并不是通過繼承關系來實作的,而是采用組合實作的。

JVM類加載機制

啟動類加載器:Bootstrap ClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,并且能被虛拟機識别的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動類加載器是無法被Java程式直接引用的。

擴充類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實作,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充類加載器。

應用程式類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實作,它負責加載使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中預設的類加載器。

應用程式都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因為JVM自帶的ClassLoader隻是懂得從本地檔案系統加載标準的java class檔案,是以如果編寫了自己的ClassLoader,便可以做到如下幾點:

1)在執行非置信代碼之前,自動驗證數字簽名。

2)動态地建立符合使用者特定需要的定制化建構類。

3)從特定的場所取得java class,例如資料庫中和網絡中。

JVM類加載機制

•全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也将由該類加載器負責載入,除非顯示使用另外一個類加載器來載入

•父類委托,先讓父類加載器試圖加載該類,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類

•緩存機制,緩存機制将會保證所有加載過的Class都會被緩存,當程式中需要使用某個Class時,類加載器先從緩存區尋找該Class,隻有緩存區不存在,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩存區。這就是為什麼修改了Class後,必須重新開機JVM,程式的修改才會生效

4 類的加載

類加載有三種方式:

1、Dog dog = new Dog(); 

由new 關鍵字建立一個類的執行個體。

這個動作會導緻常量池的解析,Dog類被隐式裝載。 

如果目前ClassLoader無法找到Dog,則抛出NoClassDefFoundError。 

2、Class clazz = Class.forName(“Dog”); 

   Object dog =clazz.newInstance(); 

通過反射加載類型,并建立對象執行個體 

如果無法找到Dog,則抛出ClassNotFoundException。 

3、Class clazz = classLoader.loadClass(“Dog”); 

(擷取目前線程的ClassLoader :  Thread.currentThread().getContextClassLader())

那麼,1和2和3究竟有什麼差別呢?分别用于什麼情況呢? 

1和2使用的類加載器是相同的,都是目前類加載器。(即:this.getClass.getClassLoader)。 

3由使用者指定類加載器。 

如果需要在目前類路徑以外尋找類,則隻能采用第3種方式。第3種方式加載的類與目前類分屬不同的命名空間。 

目前類加載器命名空間對其不可見。當然,如果被加載類的超類對于目前類命名空間可見的話,則可以進行強制轉型。 

第1和第2種情況差別不大。如果,Dog類在編譯時無法得到,則使用第2種方式。 

另外,第1種和第2種都會導緻類被初始化,即:執行類的靜态初始化語句,而第3種情況不會。 

另外注意,第1種抛出Error,第2、3種抛出Exception,它們分屬于不同的異常/錯誤分支。 

----------------------------- 

對象執行個體化方式: 

1、new Dog(); 

2、clazz.newInstance();或者clazz.getConstructor(...).newInstance(...); 

3、Object.clone();//通過本地方法進行複制 

4、反序列化  

例子:

package test4;


public class Main {
    public static void main(String[] args)throws ClassNotFoundException {
        ClassLoader loader = Main.class.getClassLoader(); 
        System.out.println(loader); 
        
        //使用ClassLoader.loadClass()來加載類,不會執行初始化塊 
       // loader.loadClass("test4.Test2"); 
        
        //使用Class.forName()來加載類,預設會執行初始化塊 
       //Class.forName("test4.Test2"); 
        
        //使用Class.forName()來加載類,并指定ClassLoader,初始化時不執行靜态塊 
        Class.forName("test4.Test2", false, loader); 
    }
}

 class Test2 { 
    static { 
            System.out.println("靜态初始化塊執行了!"); 
    } 
}      

Class.forName()和ClassLoader.loadClass()差別

Class.forName():将類的.class檔案加載到jvm中之外,還會對類進行解釋,執行類中的static塊;

ClassLoader.loadClass():隻幹一件事情,就是将.class檔案加載到jvm中,不會執行static中的内容,隻有在newInstance才會去執行static塊。

注:

Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。并且隻有調用了newInstance()方法采用調用構造函數,建立類的對象 。

5 雙親委派模型

雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,是以,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,隻有當父加載器在它的搜尋範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類,否則,子類不會去加載。

 能夠很好的統一管理類加載,首先交給上級,如果上級有了,就加載,這樣如果之前已經加載過的類,這時候在來加載它的時候隻要拿過來用就可以了,無需二次加載了

委托機制的意義 — 防止記憶體中出現多份同樣的位元組碼 

比如兩個類A和類B都要加載System類:

如果不用委托而是自己加載自己的,那麼類A就會加載一份System位元組碼,然後類B又會加載一份System位元組碼,這樣記憶體中就出現了兩份System位元組碼。

如果使用委托機制,會遞歸的向父類查找,也就是首選用Bootstrap嘗試加載,如果找不到再向下。這裡的System就能在Bootstrap中找到然後加載,如果此時類B也要加載System,也從Bootstrap開始,此時Bootstrap發現已經加載過了System那麼直接傳回記憶體中的System即可而不需要重新加載,這樣記憶體中就隻有一份System的位元組碼了。

問 : 能不能自己寫個類叫java.lang.System?

答案:通常不可以,但可以采取另類方法達到這個需求。 

解釋:為了不讓我們寫System類,類加載采用委托機制,這樣可以保證父加載器優先,父加載器能找到的類,子加載器就沒有機會加載。而System類是Bootstrap加載器加載的,就算自己重寫,也總是使用Java系統提供的System,自己寫的System類根本沒有機會得到加載。

但是,我們可以自己定義一個類加載器來達到這個目的,為了避免雙親委托機制,這個類加載器也必須是特殊的。由于系統自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄,那麼系統的加載器就無法加載,也就是最終還是由我們自己的加載器加載。

1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

2、當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裡未查找到該class),會使用ExtClassLoader來嘗試加載;

4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。

ClassLoader源碼分析public Class<?> loadClass(String name)throws ClassNotFoundException {

return loadClass(name, false);
    }
    
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判斷該類型是否已經被加載
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載
                try {
                    if (parent != null) {
                         //如果存在父類加載器,就委派給父類加載器加載
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父類加載器無法完成加載請求
                   
                }
          if(c == null) {
           // 在父類加載器無法加載的時候 再調用自身的findClass方法來進行加載      
             c = findClass(name);      
}
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }      

6 打破雙親委派模型

如上,類加載器,具體來說在方法 loadClass(String,boolean)中

注釋和代碼都很清楚

1.尋找已加載的類

2.若沒有找到,則去parent中找

3.調用findClass方法

4.要是還沒有則抛出異常

是以可以這麼做:

1.在構造類加載器時将parent設為null(構造函數參數為null即可)

2.重寫loadClass(String,boolean)方法,改變類的查找機制。

繼續閱讀