虛擬機類加載機制:虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型。
Java語言里,類型的加載和連接過程是在程序運行期間完成的。
類的生命周期:
加載 loading
驗證 verification
准備 preparation
解析 resolution
初始化 initialization
使用 using
卸載 unloading
有且只有以下四種情況必須立即對類進行”初始化”(稱為對一個類進行主動引用):
- 遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時(使用new實例化對象的時候、讀取或設置一個類的靜態字段、調用一個類的靜態方法)。
- 使用java.lang.reflet包的方法對類進行反射調用的時候。
- 當初始化一個類的時候,如果發現其負類沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啟動時,虛擬機會初始化主類(包含main方法的那個類)。
被動引用:
- 通過子類引用父類的靜態字段,不會導致子類初始化(對於靜態字段,只有直接定義這個字段的類才會被初始化)。
- 通過數組定義類應用類:ClassA [] array=new ClassA[10]。觸發了一個名為[LClassA的類的初始化,它是一個由虛擬機自動生成的、直接繼承於Object的類,創建動作由字節碼指令newarray觸發。
- 常量會在編譯階段存入調用類的常量池。
編譯器會為接口生成<clinit>()構造器,用於初始化接口中定義的成員變量。一個接口在初始化時,並不要求其父類接口全部完成了初始化,只有在真正使用到父接口的時候才會初始化。
1. 加載
- 通過一個類的全限定名來獲取此類的二進制字節流。
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- 在java堆中生成一個代表這個類的Class對象,作為方法區這些數據的訪問入口。
2. 驗證
驗證:確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
虛擬機規范:如果驗證到輸入的字節流不符合Class文件的存儲格式,就拋出一個java.lang.VerifyError異常或其子類異常。
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范,並且能被當前版本的虛擬機處理。這個階段的驗證時給予字節流進行的,經過了這個階段的驗證之后,字節流才會進入內存的方法區中進行存儲所以后面的驗證階段都是給予方法區的存儲結構進行的。
- 元數據驗證:對類的元數據信息進行語義校驗,保證不存在不符合java語言規范的元數據信息。
- 字節碼驗證:進行數據流和控制流分析,對類的方法體進行校驗分析,保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。
- 符號引用驗證:發生在虛擬機將符號引用轉化為直接引用的時候(解析階段),對常量池中的各種符號引用的信息進行匹配性的校驗。
3. 准備
准備階段是正式為類變量分配內存並設置類變量初始值(各數據類型的零值)的階段,這些內存將在方法區中進行分配。但是如果類字段的字段屬性表中存在ConstantValue屬性,那在准備階段變量值就會初始化為ConstantValue屬性指定的值。
public static final int value=122;
4. 解析
解析階段是在虛擬機將常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標並不一定已經加載到內存中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
A. 類或接口(對應於常量池的CONSTANT_Class_info類型)的解析:
假設當前代碼所處的類為D,需要將一個從未解析過的符號引用N解析為一個類或接口C的直接引用:
- 如果C不是一個數組類型,虛擬機將會把代表C的全限定名傳遞給D的類加載器去加載這個類。
- 如果C是一個數組類型,並且數組的元素類型為對象(N的描述符類似[Ljava.lang.Integer),將會加載數組元素類型(java.lang.Integer),接着由虛擬機生成一個代表此數組維度和元素的數組對象。
- 如果以上過程沒有發生異常,則C在虛擬機中已經成為了一個有效的類和接口了,之后還要進行的是符號引用驗證,確認D是否具有對C的訪問權限,如果沒有,將拋出java.lang.IllegalAccessError異常。
B. 字段(對應於常量池的CONSTANT_Fieldref_info類型)解析:
- 對字段表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個字段所屬的類或接口。
- 如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用。
- 否則,如果C實現了接口,則會按照繼承關系從下往上遞歸搜索各個接口和他的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用。
- 否則,如果C不是java.lang.Object類型的話,將會按照繼承關系從下往上遞歸的搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用。
- 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
虛擬機的編譯器實現可能會更嚴格:如果一個同名字段同時出現在C實現的接口和父類中,或者同時在自己或父類的多個接口中出現,編譯器將可能拒絕編譯。
C. 類方法(對應於常量池的CONSTANT_Methodref_info類型)解析:
- 對方法表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個方法所屬的類或接口。
- 類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個接口,則拋出java.lang.IncompatibleClassChangeError。
- 在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用。
- 否則,在C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用。
- 否則,在C實現的接口列表及它們的父接口中遞歸的查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有說明C是個抽象類,查找結束,拋出java.lang.AbstractMethodError異常。
- 否則,查找失敗,拋出java.lang.NoSuchMethodError異常。
- 如果查找返回了直接引用,將會對這個方法進行權限驗證,如果發現不具備對這個方法的訪問權限,則拋出java.lang.IllegalAccessError異常。
D. 接口方法(對應於常量池的CONSTANT_InterfaceMethodref_info類型):
- 對方法表中的class_index項中索引的CONSTANT_Class_info符號引用進行解析。用C表示這個方法所屬的類或接口。
- 如果在接口方法表中發現class_index中索引的C是個類,則拋出java.lang.IncompatibleClassChangeError。
- 否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用。
- 否則,在接口C的父接口中遞歸查找,知道java.lang.Object類(包括在內),看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用。
- 否則,查找失敗,拋出java.lang.NoSuchMethodError。
5. 初始化
初始化階段是執行類構造器<clinit>()方法的過程。
- <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合並產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的。靜態語句塊只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。
2. 方法與實例構造器<init>()不同,不需要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()已經執行完畢。
3. <clinit>()方法對於類或接口來說不是必須的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
4. 執行接口的<clinit>()不需要先執行父接口的<clinit>()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。接口的實現類在初始化時也不會執行接口的<clinit>()方法。
5. 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖和同步,如果多個線程同時去初始化一個類,則只會有一個線程去執行這個類的<clinit>()方法,其他線程需要阻塞等待。