類加載機(jī)制——虛擬機(jī)把描述類的數(shù)據(jù)從class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗,轉(zhuǎn)換解析,和初始化,最終形成可以被虛擬機(jī)直接使用的java類型。
類從加載進(jìn)內(nèi)存中到卸載除內(nèi)存中,整個生命周期:
加載-驗證-準(zhǔn)備-解析-初始化-使用-卸載
其中 驗證-準(zhǔn)備-解析 統(tǒng)稱連接。加載、驗證、準(zhǔn)備、初始化、卸載的順序是固定的,而解析階段是不一定的:他在某些情況下可以在初始化之后再開始,這是為了支持java語言的動態(tài)綁定。
初始化的時機(jī)
虛擬機(jī)嚴(yán)格規(guī)定了有且只有5種情況必須立即對類進(jìn)行初始化:
- 遇到new、 getstatic、putstatic、invokestatic這四條字節(jié)碼指令時,對應(yīng)的場景是:使用new關(guān)鍵字實例化對象的時候,讀取、設(shè)置一個類的靜態(tài)變量(被final修飾、已在編譯期放入常量池的靜態(tài)變量除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)初始化。
- 當(dāng)初始化一個子類的時候,如果其父類沒有進(jìn)行初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main方法的那個類),虛擬機(jī)會先初始化這個主類。
- 當(dāng)使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_inokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化
加載
加載的三步:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的lava.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
數(shù)組類本身不是通過類加載器創(chuàng)建,它是由java虛擬機(jī)直接創(chuàng)建的。
加載階段和連接階段可能交叉進(jìn)行。
驗證
驗證的目的是為了確保class文件字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。
- 文件格式驗證
第一階段腰驗證字節(jié)流是否符合class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。這一階段包括:- 是否以魔數(shù)0xCAFEBABE開頭
- 主次版本號是否在當(dāng)前虛擬機(jī)處理范圍內(nèi)
- 常量池的常量種是否有不被支持的常量類型
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
- class文件中各個部分及文件本身是否有被刪除的或附加的的其他信息
。。。。。。
驗證后的字節(jié)流內(nèi)存的方法區(qū)進(jìn)行存儲,后面的3個驗證是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的,不會再直接操作字節(jié)流。
-
元數(shù)據(jù)驗證
第二階段對字節(jié)碼描述的信息進(jìn)行語義分析,以保證,以保證其描述的信息符合java語言規(guī)范的要求,保證不存在不符合java規(guī)范的元數(shù)據(jù)信息。這個階段可能包括的驗證點(diǎn)如下:- 這個類是否有父類。(除了Object都應(yīng)當(dāng)有父類)
- 這個類的父類是否繼承了不被允許繼承的父類(被final修飾的類)
- 如果這個類不是抽象類,是否實現(xiàn)了其父類或接口要求被實現(xiàn)的所有方法
- 類種的字段、方法是否與父類產(chǎn)生矛盾(覆蓋父類final字段、不符合規(guī)則的重載等)
-
字節(jié)碼驗證
第三階段是整個驗證過程中最復(fù)雜的一個階段,主要的目的是通過數(shù)據(jù)流和控制流分析,確定程序是語義合法的、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段對類的方法體進(jìn)行校驗分析,保證被校驗類的方法再運(yùn)行時不會做出危害虛擬機(jī)安全的事件。- 保證時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作。(不會出現(xiàn):操作棧放置int類型,使用時卻按long類型加入本地變量表)
- 保證跳轉(zhuǎn)指令不會調(diào)到方法體以外的字節(jié)碼指令上
- 保證方法體內(nèi)的類型轉(zhuǎn)換是有效的。
-
符號引用驗證
驗證發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)化動作將在連接的第三階段-解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗。- 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類
- 在指定類中是否存在符合方法的字段描述以及簡單名稱所描述的方法和字段。
- 符合引用中的字段、方法、類是否可以被當(dāng)前類訪問
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)進(jìn)行分配。
類變量是被static修飾的變量,而實例變量會對象實例化時隨著對象分配在java堆中。
//初始值為0
public static int value = 123;
//初始值為123
public static final int value = 123;
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
- 符號引用:是以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要再使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在java虛擬機(jī)規(guī)范的Class文件格式里
- 直接引用:可以是直接指向目標(biāo)的指針、相對偏移量或是以一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實現(xiàn)的內(nèi)存布局相關(guān)的,如果有了直接引用,那引用的目標(biāo)必定在內(nèi)存中存在。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符這7類符合引用進(jìn)行。
初始化
類初始化是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全就虛擬機(jī)主導(dǎo)和控制,到了初始化階段,才真正開始執(zhí)行類中定義的java程序代碼。
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
- <clinit>()方法是有編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器的收集順序是由語句在源文件的出現(xiàn)順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的 靜態(tài)語句塊中可以賦值,但不能訪問。
public class Test {
static {
i = 0;//可以被賦值
System.out.println();//不能訪問,報錯。
}
static int i;
}
- <clinit>()方法與類的構(gòu)造方法不同,他不需要顯式的調(diào)用父類構(gòu)造器,虛擬機(jī)會保證子類的<clinit>()執(zhí)行前,父類的<clinit>()已經(jīng)執(zhí)行完畢。因此虛擬機(jī)執(zhí)行的第一個<clinit>()方法的類是Object。
- 由于父類的<clinit>()先執(zhí)行,所以父類中定義的靜態(tài)塊要優(yōu)先于子類的變量賦值操作。
static class Parent {
public static int A = 1;
static {
A = 2;
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Parent.Sub.B);
//打印為2
}
}
- <clinit>()對于類或接口并不是必須的,如果一個類沒有靜態(tài)語句塊,也沒有變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
- 接口中不能使用靜態(tài)語句塊,但仍有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當(dāng)接口的父類定義的變量使用時,父接口才會初始化,另外接口的實現(xiàn)類在初始化時一樣不會執(zhí)行接口的<clinit>()方法。
- 虛擬機(jī)會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖、同步。
- 同一個類加載器下,一個類只會初始化一次。