虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準(zhǔn)備、驗證、解析3個部分統(tǒng)稱為連接(Linking)
從ZIP包中讀取,JAR,WAR,EAR格式的基礎(chǔ)
從網(wǎng)絡(luò)中獲取,Applet應(yīng)用
運行時計算生成,動態(tài)代理技術(shù)
由其他文件生成,JSP應(yīng)用,由JSP文件生成對應(yīng)的Class類
驗證
驗證是連接階段的第一步,確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致分下面4個動作:
文件格式驗證
元數(shù)據(jù)驗證
字節(jié)碼驗證
符號引用驗證
在驗證過程中大致會拋出如下幾種錯誤:
IncompatibleClassChangeError
Unsupported major.minor version
IllegalAccessError
NoSuchFieldError
NoSuchMethodError
文件格式驗證
驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。
驗證Class文件的標(biāo)識。魔數(shù)是否以0xCAFEBABE開頭
驗證Class文件的版本號。主次版本號是否在當(dāng)前虛擬機處理范圍之內(nèi)
驗證常量池。常量池的常量中是否有不被支持的常量類型
指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)
Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息
通過了這個階段的驗證后,字節(jié)流才會進入內(nèi)存的方法區(qū)中進行存儲。后面的驗證會基于方法區(qū)的存儲結(jié)構(gòu)進行驗證,而不再操作字節(jié)流進行驗證。
元數(shù)據(jù)驗證
該階段主要對字節(jié)碼描述的信息進行語義分析,以保證符合Java語言規(guī)范要求。
類是否有父類(除了java.lang.Object外,所有類都應(yīng)當(dāng)有父類)
類的父類是否繼承了不允許被繼承的類(final修飾的類)
如果類不是抽象類,是否實現(xiàn)了器父類或接口中要求實現(xiàn)的所有方法
類中的字段,方法是否與父類產(chǎn)生矛盾(如:覆蓋了父類的final字段)
字節(jié)碼驗證
該階段主要是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的。
對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。
符號引用驗證
符號引用驗證是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,該階段在虛擬機將符號引用轉(zhuǎn)化為直接引用時發(fā)生。
符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類。
在指定類中是否存在符合方法的字段描述以及簡單名稱所描述的方法和字段。
符號引用中的類,字段,方法的訪問性(private,protected,public,default)是否可被當(dāng)前類訪問。
符號引用驗證的目的就是確保解析動作能正常執(zhí)行。
準(zhǔn)備
準(zhǔn)備階段是為類變量分配內(nèi)存并設(shè)置類變量初始化的階段,這些變量所使用的內(nèi)存當(dāng)將在方法區(qū)中進行分
配。只對類變量進行內(nèi)存分配(static修飾),不包括實例變量,實例變量將會在對象實例化是隨著對象一起分配在Java堆中。
如:一個類變量的定義為
// n的初始化值是0,而不是2。因為這個時候還沒執(zhí)行任何初始化方法(<clinit>)。 public static int n = 2;
再例如:
// 編譯時會為m生成ConstantValue屬性,在準(zhǔn)備階段會根據(jù)ConstantValue將m值設(shè)置為2 public static final int m = 2;
類變量和實例變量
類變量:也稱為靜態(tài)變量,在類中以static關(guān)鍵字聲明,但必須在方法構(gòu)造方法和語句塊之外
實例變量:屬于該類的對象,必須產(chǎn)生該類對象,才能調(diào)用實例變量。
解析
解析的目的就是將常量池內(nèi)的符號引用替換為直接引用。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。符號引用的字面量形式已經(jīng)明確定義在Java虛擬機規(guī)范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針,相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用與虛擬機實現(xiàn)的內(nèi)存布局相關(guān),同一個符號引用在不用虛擬機實例上翻譯出來的直接引用一般不同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
一句話總結(jié):符號引用是以字面量的形式明確定義在常量池中;直接引用是指向目標(biāo)的指針,或者相對偏移量。
解析動作主要對類或者接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行,分別對應(yīng)于常量池
CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_InterfaceMethodref_info
CONSTANT_MethodType_info
CONSTANT_Methodref_info
CONSTANT_MethodHandler_info
CONSTANT_invokeDynamic_info
字段的解析
class A extends B implements C, D{
private String str; //字段的解析
}
解析字段的順序:
①先查找本類A,如果包含了簡單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個字段的直接引用,查找結(jié)束。
②否則,在接口中查找。將會按照集成關(guān)系從下往上遞歸搜搜各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都于目標(biāo)相匹配的字段,則返回這個字段的直接引用,查找結(jié)束。
③否則,在父類中查找,如果在父類中包含了簡單名稱和字段描述符都于目標(biāo)相匹配的字段,則返回這個字段的直接引用,查找結(jié)束
④否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
類方法的解析
class A extends B implements C, D{
private void inc(); //方法的解析
}
①如果在類方法表中發(fā)現(xiàn)class_index中索引的A是一個接口,哪就直接拋出java.lang.IncompatiableClassChangeError異常。
②如果通過了第一步,先查找本類A,是否由簡單名稱和描述符都于目標(biāo)相匹配的方法,如果有則返回方法的直接引用,查找結(jié)束。
③否則,父親中遞歸查找是否又簡單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個方法的直接引用,查找結(jié)束。
④否則,在類實現(xiàn)的接口列表及它們的父接口之中查找是否有簡單名名稱和描述符都與目標(biāo)相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,這時查找結(jié)束,拋出java.lang.AbastractMethodError異常。
⑤否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。
接口方法的解析
與類的方法解析不同,如果在接口方法表中發(fā)現(xiàn)class_index中的索引A是個類而不是接口,那就直接拋出java.lang.IncompatiableClassError異常。
否則,先查找本接口,是否有簡單名稱和描述符都與目標(biāo)匹配的方法,如果有則返回這個方法的直接引用,查找結(jié)束。
否則,在接口的父接口中遞歸查找,直到j(luò)ava.lang.Object類(查找范圍包括Object類)為止,看是否有簡單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個方法的直接引用,查找結(jié)束。
否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。
初始化
<clinit> 類的初始化。靜態(tài)變量,靜態(tài)塊的初始化。所有的類變量初始化語句和類型的靜態(tài)初始化器。
Java在編譯之后會在字節(jié)碼文件中生成<clinit>方法,稱之為類構(gòu)造器,類構(gòu)造器同實例構(gòu)造器一樣,也會對靜態(tài)語句塊,靜態(tài)變量進行初始化
<init> 對象的初始化
Java在編譯之后會在字節(jié)碼文件中生成<init>方法,稱之為實例構(gòu)造器。該實例構(gòu)造器會對語句塊,變量進行初始化,并調(diào)用父類的構(gòu)造器。
<clinit>方法是在類加載過程中執(zhí)行的,而<init>是在對象實例化執(zhí)行的,所以<clinit>一定比<init>先執(zhí)行。所以整個順序就是:
父類靜態(tài)變量初始化
父類靜態(tài)語句塊
子類靜態(tài)變量初始化
子類靜態(tài)語句塊
父類變量初始化
父類語句塊
父類構(gòu)造函數(shù)
子類變量初始化
子類語句塊
子類構(gòu)造函數(shù)