一文讓你理解Class類加載機制

理解類加載機制

Class文件是各類編譯器編譯生成的二進制文件,在Class文件中描述了各類與該類相關的信息,可是Class文件自己是一個靜態的東西,想要使用某個類的話,須要java虛擬機將該類對應的Class文件加載進虛擬機中以後才能進行運行和使用。java

舉個例子,Class文件就比如是各個玩具設計商提供的設計方案,這些方案自己是不能直接給小朋友玩的,須要玩具生產商根據方案的相關信息製造出具體的玩具才能夠給小朋友玩。那麼不一樣的設計商有他們本身的設計思路,只要最終設計出來的方案符合生產商生產的要求便可。生產商在生產玩具時,首先會根據本身的生產標準對設計商提交來的方案進行閱讀,審覈,校驗等一系列步驟,若是該方案符合生產標準,則會根據方案建立出對應的模具,當經銷商須要某個玩具時,生產商則拿出對應的模具生產出具體的玩具,而後把玩具提交給經銷商。數組

對於java而言,虛擬機就是玩具生產商,設計商提交過來的方案就是一個個的Class文件,方案建立的模具就 總的來講,類的加載過程,包括卸載在內的整個生命週期共有如下7個階段:安全

加載、驗證、準備、初始化、卸載這5個階段的順序是肯定的,可是解析階段不必定,在某些狀況下解析能夠在初始化以後再執行,爲了支持java的運行時綁定,也成爲動態綁定或晚期綁定。invokedynamic指令就是用於動態語言支持,這裏「動態」的含義是必須等到城市實際運行到這條指令的時候,解析動做纔開始執行。bash

加載

「加載」是「類加載」過程當中的一個階段,在加載階段,虛擬機須要作如下3件事情:數據結構

  • 經過類的全限定名得到該類的二進制字節流多線程

  • 將這個字節流所表明的靜態存儲結構轉換成方法區中的某個運行時數據結構spa

  • 在方法區內存(對於HotSpot虛擬機)中生成一個表明該類的java.lang.Class對象,做爲訪問方法區中該類的運行時數據結構的外部接口線程

加載階段中「經過類的全限定名得到該類的二進制字節流」這個動做,被放到java虛擬機外部實現,目的是最大限度的讓應用程序去決定該如何獲取所需的類,而實現該動做的代碼模塊就是類加載器(ClassLoader),JVM提供了3種類加載器:設計

  • 啓動類加載器(Bootstrap ClassLoader):負責加載 JAVAHOME\lib 目錄中的,或經過-Xbootclasspath參數指定路徑中的,且被虛擬機承認(按文件名識別,如rt.jar)的類。指針

  • 擴展類加載器(Extension ClassLoader):負責加載 JAVAHOME\lib\ext 目錄中的,或經過java.ext.dirs系統變量指定路徑中的類庫。

  • 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中了。

驗證

加載完成後,緊接着(更確切的說是交叉執行)虛擬機會對加載的字節流進行驗證。虛擬機若是不檢查輸入的字節流,對其安全信任的話,極可能會由於載入了有害的字節流而致使系統崩潰。驗證階段大體會完成4中不一樣的檢驗動做:

文件格式驗證

文件格式驗證主要是校驗該字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機所接受。這個階段包括但不限於如下驗證點:

  • 是否以魔數0xCAFEBABE開頭
  • 主、次版本號是否在當前虛擬機處理的範圍以內
  • 常量池中是否有不支持的常量類型(經過tag校驗)
  • 常量的索引是否有指向不存在或不符合類型的常量
  • ...

元數據驗證

元數據驗證主要是對字節流中的描述信息(描述符)進行語義分析,以確保其描述的信息符合java語言規範的要求。這個階段包括但不限於如下驗證點:

  • 這個類是否有父類,除了java.lang.Object,全部的類都應該有父類
  • 這個類的父類是否繼承了不容許被繼承的類,如被final修飾的類
  • 若是這個類不是抽象類,是否實現了父類或接口中要求的全部的方法
  • ...

字節碼驗證

字節碼驗證主要是對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發生。這個階段包括但不限於如下驗證點:

  • 操做數棧的數據類型與指令碼中所需類型是否相符
  • 校驗跳轉指令是否會跳轉到方法體之外的字節碼指令上
  • 校驗方法體中類型轉換是不是有效的
  • ...

符號引用驗證

符號引用驗證主要是對類自身之外的信息進行匹配新校驗,包括常量池中的各類符號引用。這個階段包括但不限於如下驗證點:

  • 符號引用中經過字符串描述的全限定類名是否能找到對應的類
  • 在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的字段和方法
  • ...

準備

準備階段是爲類變量(static)在方法區中分配內存並設置初始值(默認值,如int的默認值爲0)的階段。實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。爲類變量設置初始值跟該變量是否有final修飾符有關係。 若是沒用final進行修飾,以下列的代碼:

// 準備階段執行完成後,value變量的值爲int的「零值」,即:0
// 把value賦值爲10的putstatic指令是程序被編譯後,
// 存放於類構造器<clinit>()方法中的,因此具體賦值的操做會在初始化階段執行
public static int value = 10;
複製代碼

若是使用了final進行修飾,以下列的代碼:

// 若是類字段的字段屬性表中有ConstantValue屬性,
// 則會在準備階段將變量的值初始化爲ConstantValue所指定的值
// 準備階段執行完成後,VALUE變量的值被賦值爲20
public final static int VALUE = 20;
複製代碼

解析

解析階段是虛擬機將常量池中的符號引用替換爲直接引用的過程,在該階段將會進行符號引用的校驗。 符號引用是Class文件中用來描述所引用目標的符號,能夠是任何形式的字面量。 直接引用是虛擬機在內存中引用具體類或接口的,能夠是直接指向目標的指針,相對偏移量或者是一個能間接定位到目標的句柄。 簡單的來講,符號引用是Class類文件用來定位目標的,直接引用是虛擬機用來在內存中定位目標的。

初始化

初始化階段是執行類的構造器方法()的過程,類的構造方法由類變量的賦值動做和靜態語句塊按照在源文件中出現的順序合併而成,該合併操做由編譯器完成。

  • ()方法對於類或接口不是必須的,若是一個類中沒有靜態代碼塊,也沒有靜態變量的賦值操做,那麼編譯器不會生成()方法
  • ()方法與實例構造器方法()不一樣,不須要顯式的調用父類的方法,虛擬機會保證父類的優先執行
  • 爲了防止屢次執行,虛擬機會確保方法在多線程環境下被正確的加鎖同步執行,若是有多個線程同時初始化一個類,那麼只有一個線程可以執行方法,其它線程進行阻塞等待,直到執行完成
  • 執行接口的方法是不須要先執行父接口的,只有使用父接口中定義的變量時,纔會執行。

java虛擬機規範嚴格規定了有且只有一下5中狀況必須當即對類進行初始化:

  1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,生成這4條指令的最多見的java代碼場景是:使用new實例化對象時,讀取或設置類的靜態字段時,調用一個類的靜態方法時
  2. 使用java.lang.reflect對類進行反射調用時,如經過Class.forName()建立對象時
  3. 當初始化一個類時,若是父類尚未初始化,則要先觸發父類的初始化,即先要執行父類的構造器方法()
  4. 啓動虛擬機時,須要初始化包含main方法的類
  5. 在JDK1.7中,若是java.lang.invoke.MethodHandler實例最後的解析結果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有進行初始化

如下幾種狀況,不會觸發類初始化:

  • 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
class Parent {
    static int a = 100;    
    static   {        
        System.out.println("parent init!");    
    }
}
class Child extends Parent {    
    static {        
        System.out.println("child init!");    
    }
}
public class Init{      
    public static void main(String[] args){  
        // 只會執行父類的初始化,不會執行子類的初始化
        // 將打印:parent init!
        System.out.println(Child.a);      
    }
}
複製代碼
  • 定義對象數組,不會觸發該類的初始化。
public class Init{      
    public static void main(String[] args){ 
        // 不會有任何輸出
        Parent[] parents = new Parent[10];    
    }  
}
複製代碼
  • 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類的初始化。
class Const {    
    static final int A = 100;    
    static {        
        System.out.println("Const init");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // Const.A會存入Init類的常量池中,調用時並不會觸發Const類的初始化
        // 將打印:100
        System.out.println(Const.A);      
    }  
}
複製代碼
  • 經過類名獲取Class對象,不會觸發類的初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // 不會打印任何信息
        Class catClazz = Class.class;      
    }  
}
複製代碼
  • 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不會打印任何信息
        Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());      
    }  
}
複製代碼
  • 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不會打印任何信息
        new ClassLoader(){}.loadClass("com.test.Cat");      
    }  
}
複製代碼

最後,附上一幅Class類加載過程的思惟導圖:

是Class文件加載進虛擬機中的類,生產的玩具就是類的實例對象。

所以,從Class文件到對象須要通過的步驟大體爲: Class文件-->類-->實例對象 而類的加載機制,就是負責將Class文件轉換成虛擬機中的類的一個過程。

總的來講,類的加載過程,包括卸載在內的整個生命週期共有如下7個階段:

加載、驗證、準備、初始化、卸載這5個階段的順序是肯定的,可是解析階段不必定,在某些狀況下解析能夠在初始化以後再執行,爲了支持java的運行時綁定,也成爲動態綁定或晚期綁定。invokedynamic指令就是用於動態語言支持,這裏「動態」的含義是必須等到城市實際運行到這條指令的時候,解析動做纔開始執行。

加載

「加載」是「類加載」過程當中的一個階段,在加載階段,虛擬機須要作如下3件事情:

  • 經過類的全限定名得到該類的二進制字節流

  • 將這個字節流所表明的靜態存儲結構轉換成方法區中的某個運行時數據結構

  • 在方法區內存(對於HotSpot虛擬機)中生成一個表明該類的java.lang.Class對象,做爲訪問方法區中該類的運行時數據結構的外部接口

加載階段中「經過類的全限定名得到該類的二進制字節流」這個動做,被放到java虛擬機外部實現,目的是最大限度的讓應用程序去決定該如何獲取所需的類,而實現該動做的代碼模塊就是類加載器(ClassLoader),JVM提供了3種類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):負責加載 JAVAHOME\lib 目錄中的,或經過-Xbootclasspath參數指定路徑中的,且被虛擬機承認(按文件名識別,如rt.jar)的類。

  • 擴展類加載器(Extension ClassLoader):負責加載 JAVAHOME\lib\ext 目錄中的,或經過java.ext.dirs系統變量指定路徑中的類庫。

  • 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中了。

驗證

加載完成後,緊接着(更確切的說是交叉執行)虛擬機會對加載的字節流進行驗證。虛擬機若是不檢查輸入的字節流,對其安全信任的話,極可能會由於載入了有害的字節流而致使系統崩潰。驗證階段大體會完成4中不一樣的檢驗動做:

文件格式驗證

文件格式驗證主要是校驗該字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機所接受。這個階段包括但不限於如下驗證點:

  • 是否以魔數0xCAFEBABE開頭
  • 主、次版本號是否在當前虛擬機處理的範圍以內
  • 常量池中是否有不支持的常量類型(經過tag校驗)
  • 常量的索引是否有指向不存在或不符合類型的常量
  • ...

元數據驗證

元數據驗證主要是對字節流中的描述信息(描述符)進行語義分析,以確保其描述的信息符合java語言規範的要求。這個階段包括但不限於如下驗證點:

  • 這個類是否有父類,除了java.lang.Object,全部的類都應該有父類
  • 這個類的父類是否繼承了不容許被繼承的類,如被final修飾的類
  • 若是這個類不是抽象類,是否實現了父類或接口中要求的全部的方法
  • ...

字節碼驗證

字節碼驗證主要是對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發生。這個階段包括但不限於如下驗證點:

  • 操做數棧的數據類型與指令碼中所需類型是否相符
  • 校驗跳轉指令是否會跳轉到方法體之外的字節碼指令上
  • 校驗方法體中類型轉換是不是有效的
  • ...

符號引用驗證

符號引用驗證主要是對類自身之外的信息進行匹配新校驗,包括常量池中的各類符號引用。這個階段包括但不限於如下驗證點:

  • 符號引用中經過字符串描述的全限定類名是否能找到對應的類
  • 在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的字段和方法
  • ...

準備

準備階段是爲類變量(static)在方法區中分配內存並設置初始值(默認值,如int的默認值爲0)的階段。實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。爲類變量設置初始值跟該變量是否有final修飾符有關係。 若是沒用final進行修飾,以下列的代碼:

// 準備階段執行完成後,value變量的值爲int的「零值」,即:0
// 把value賦值爲10的putstatic指令是程序被編譯後,
// 存放於類構造器<clinit>()方法中的,因此具體賦值的操做會在初始化階段執行
public static int value = 10;
複製代碼

若是使用了final進行修飾,以下列的代碼:

// 若是類字段的字段屬性表中有ConstantValue屬性,
// 則會在準備階段將變量的值初始化爲ConstantValue所指定的值
// 準備階段執行完成後,VALUE變量的值被賦值爲20
public final static int VALUE = 20;
複製代碼

解析

解析階段是虛擬機將常量池中的符號引用替換爲直接引用的過程,在該階段將會進行符號引用的校驗。 符號引用是Class文件中用來描述所引用目標的符號,能夠是任何形式的字面量。 直接引用是虛擬機在內存中引用具體類或接口的,能夠是直接指向目標的指針,相對偏移量或者是一個能間接定位到目標的句柄。 簡單的來講,符號引用是Class類文件用來定位目標的,直接引用是虛擬機用來在內存中定位目標的。

初始化

初始化階段是執行類的構造器方法()的過程,類的構造方法由類變量的賦值動做和靜態語句塊按照在源文件中出現的順序合併而成,該合併操做由編譯器完成。

  • ()方法對於類或接口不是必須的,若是一個類中沒有靜態代碼塊,也沒有靜態變量的賦值操做,那麼編譯器不會生成()方法
  • ()方法與實例構造器方法()不一樣,不須要顯式的調用父類的方法,虛擬機會保證父類的優先執行
  • 爲了防止屢次執行,虛擬機會確保方法在多線程環境下被正確的加鎖同步執行,若是有多個線程同時初始化一個類,那麼只有一個線程可以執行方法,其它線程進行阻塞等待,直到執行完成
  • 執行接口的方法是不須要先執行父接口的,只有使用父接口中定義的變量時,纔會執行。

java虛擬機規範嚴格規定了有且只有一下5中狀況必須當即對類進行初始化:

  1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,生成這4條指令的最多見的java代碼場景是:使用new實例化對象時,讀取或設置類的靜態字段時,調用一個類的靜態方法時
  2. 使用java.lang.reflect對類進行反射調用時,如經過Class.forName()建立對象時
  3. 當初始化一個類時,若是父類尚未初始化,則要先觸發父類的初始化,即先要執行父類的構造器方法()
  4. 啓動虛擬機時,須要初始化包含main方法的類
  5. 在JDK1.7中,若是java.lang.invoke.MethodHandler實例最後的解析結果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有進行初始化

如下幾種狀況,不會觸發類初始化:

  • 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
class Parent {
    static int a = 100;    
    static   {        
        System.out.println("parent init!");    
    }
}
class Child extends Parent {    
    static {        
        System.out.println("child init!");    
    }
}
public class Init{      
    public static void main(String[] args){  
        // 只會執行父類的初始化,不會執行子類的初始化
        // 將打印:parent init!
        System.out.println(Child.a);      
    }
}
複製代碼
  • 定義對象數組,不會觸發該類的初始化。
public class Init{      
    public static void main(String[] args){ 
        // 不會有任何輸出
        Parent[] parents = new Parent[10];    
    }  
}
複製代碼
  • 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類的初始化。
class Const {    
    static final int A = 100;    
    static {        
        System.out.println("Const init");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // Const.A會存入Init類的常量池中,調用時並不會觸發Const類的初始化
        // 將打印:100
        System.out.println(Const.A);      
    }  
}
複製代碼
  • 經過類名獲取Class對象,不會觸發類的初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args){ 
        // 不會打印任何信息
        Class catClazz = Class.class;      
    }  
}
複製代碼
  • 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不會打印任何信息
        Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader());      
    }  
}
複製代碼
  • 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做
class Cat {    
    private string name;    
    static {        
        System.out.println("Cat is loaded");    
    }
}
public class Init{      
    public static void main(String[] args) throws ClassNotFoundException{ 
        // 不會打印任何信息
        new ClassLoader(){}.loadClass("com.test.Cat");      
    }  
}
複製代碼

最後,附上一幅Class類加載過程的思惟導圖:

相關文章
相關標籤/搜索