【009】【JVM——類加載機制】

JVM——類加載機制

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載 7 個階段。其中驗證、準備、解析3個部分統稱爲鏈接。java

加載、驗證、準備、初始化和卸載這個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定: 它在某些狀況下能夠在初始化階段以後再開始。數據庫

1、類初始化的觸發

對於初始化階段,虛擬機規範嚴格規定了有且只有5 種狀況必須當即對類進行「初始化」,加載、驗證、準備在初始化以前已經進行。這5種方式稱爲對類的主動引用。數組

    1. 遇到new 、getstatic 、putstatic 或invokestatic 這4 條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化. 生成這4 條指令的最多見的Java 代碼場景是: new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final 修飾、已在編譯期把結果放入常量池的靜態字段除外),調用一個類的靜態方法。安全

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

    3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要初始化父類。數據結構

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

    5. 當使用JDK 1.7 的動態語言支持時,若是一個java.lang.invoke.MethodHandle 實例最後的解析結果REF_getStatic 、REF_putStatic 、REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,要對其進行初始化.函數

類的被動引用不會引發初始化。下面幾種類的被動引用不會引發類的初始化。佈局

    1. 經過子類引用父類的靜態字段不會致使子類初始化。編碼

    2. 經過數組定義來定義的引用類不會觸發此類的初始化。

    3. 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化

接口的不一樣特色:

接口也有初始化過程,接口中不能使用「static {}」語句塊,但編譯器仍然會爲接口生成「<clinit>()」 類構造器,用於初始化接口中所定義的接口常量。一個接口在初始化時,並不要求其父接口所有都完成了初始化, 只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。

【特別注意】下面的狀況對類和接口一樣適用。

public static final int A = 1; // 使用不會引發類初始化

public static final int B = new Integer(1); // 使用會引發類初始化

public static final Integer C = new Integer(1); // 使用會引發類初始化

public static final Integer D = 1; // 使用會引發類初始化

其它七種基本變量相似

字符串:

public static final String S = 「abc」; // 不會引發類初始化

public static final String S = new String(「abc」); // 會引發類初始化

其它狀況均會引發初始化。

2、類加載過程

加載階段,虛擬機完成如下3 件事情:

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

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

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

獲取二進制流的方式:

    1. 從ZIP,JAR 、EAR、WAR 格式中讀取。

    2. 從網絡中獲取,如Applet 。

    3. 運行時計算生成,如動態代理技術。

    4. 由其餘文件生成,JSP 文件生成對應的Class類。

    5. 從數據庫中讀取

    6. 等等

    二(一)、數組類型的加載

數組類自己不經過類加載器建立,它是由Java 虛擬機直接建立。一個數組類(下面簡稱爲C )建立過程要遵循如下規則:

    • 若是數組的組件類型(Component Type ,指的是數組去掉一個維度的類型)是引用類型,那就遞歸採用定義的加載過程去加載這個組件類型。

    • 若是數組的組件類型不是引用類型(例如int[]數組), Java 虛擬機將會把數組C 標記爲與引導類加載器關聯。

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

字節碼二進制字節流按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。加載階段與鏈接階段的部份內容是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序.

3、驗證

確保Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全.對不一樣的虛擬機言,驗證的實現可能會有所不一樣,但大多數會完成四個階段的檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證.

文件格式驗證:驗證字節流是否符合Class 文件格式的規範,而且能被當前版本的虛擬機處理. 驗證內容主要包括:

    • 是否以魔數0xCAFEBABE 開頭.

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

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

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

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

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

    • 。。。

驗證:對節碼描述的信息語義分析,以保其描述的信息符合Java 範的要求,主要是:

    • 類是否有父類(除了java.lang.Object 以外,全部的類都應當有父類).

    • 類是否繼承了不容許被繼承的類〈被final 修飾的類〉.

    • 類不是抽象類,是否實現了父類或接口之中要求實現的全部方法.

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

    • 。。。

字節碼驗證:進行數據流和控制分析.對類的方法體進行校驗分析.保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲,主要是:

    • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,

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

    • 保證方法體中的類型轉換是有效的。

    • 。。。

符號引用驗證:對類自身之外(常最池中的各類符號引用)的信息進行匹配性的校驗,主要是:

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

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

    • 符號引用中的類、字段和方法的訪問性是否可被當前類訪問.

    • 。。。

4、準備

準備階段爲類變量分配內存並設置類變量初始值,內存都在方法區中進行分配。初始值指基本數據類型的零值,引用數據類型的null。若是類字段的字段屬性表中存在ConstantValue 屬性,那在準備階段變量value 就會被初始化爲ConstantValue 屬性所指定的值。

【特別注意】下面的狀況對類和接口一樣適用。

public static final int A = 1; // 會被賦值

public static final int B = new Integer(1); // 不會被賦值

public static final Integer C = new Integer(1); // 不會被賦值

public static final Integer D = 1; //不會被賦值

其它七種基本變量相似

字符串:

public static final String S = 「abc」; //會被賦值

public static final String S = new String(「abc」); // 不會被賦值

其它狀況均會不會被賦值。

5、解析

虛擬機將常量池內的符號引用替換爲直接引用的過程。

    • 符號引用( Symbolic References ) :符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可.符號引用與虛擬機實現的內存佈局無關, 引用的目標並不必定已經加載到內存中.

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

Java虛擬機指令anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic將符號引用指向運行時常量池。執行上述任何一條指令都須要對它的符號引用的進行解析。

總的來講解析有下面幾種:

    • 類與接口解析

    • 字段解析

    • 普通方法解析

    • 接口方法解析

    • 方法類型與方法句柄解析

    • 調用點限定符解析

6、初始化

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

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

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

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

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

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

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

相關文章
相關標籤/搜索