文章作為《深入理解Java虛擬機(jī)》讀書筆記,講的可能就沒書本詳細(xì)。
一.類文件結(jié)構(gòu)
Java虛擬機(jī)多平臺
都是統(tǒng)一使用的程序存儲格式——字節(jié)碼.class文件
任何語言都可以被特定的編譯器編譯為存儲字節(jié)碼的Class文件,Class文件中包含了Java虛擬機(jī)指令集和符號表以及若干其他輔助信息。虛擬機(jī)并不關(guān)心Class的來源是何種語言。
Class文件結(jié)構(gòu)
Class文件是一組以8位字節(jié)為基礎(chǔ)單位二進(jìn)制流,各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符。
任何一個Class文件都對應(yīng)著唯一一個類或接口的定義信息,但反過來說,類或接口并不一定都得定義在文件里,類或接口也可以通過類加載器直接生成。
Class文件格式采用一種類似C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)據(jù),包含兩種數(shù)據(jù)類型:無符號數(shù)和表
魔術(shù)與Class文件的版本

使用十六進(jìn)制編譯器WinHex打開任意一個class文件,可以看到它的結(jié)構(gòu)。
前4個字節(jié)0-3表示為魔數(shù):唯一作用是確定這個文件是否為一個能被虛擬機(jī)接受的Class文件,值為0XCAFEBABE,如圖中所示
接著兩位是次版本號4-5,這里值為0x0000
接著兩位是主版本號6-7,這里值為0X0033,也就是十進(jìn)制51,表明當(dāng)前JDK版本號在1.7以上。
常量池
緊接著主次版本號之后的是常量池入口,它是占用Class文件空間最大的數(shù)據(jù)項目之一,同時還是在Class文件中第一個出現(xiàn)的表類型數(shù)據(jù)項目。
由于常量池中常量的數(shù)目是不固定的,所以在入口需要放置一項U2類型2個字節(jié)的數(shù)據(jù)代表常量池容量計數(shù)值。
這個容量計數(shù)是從1開始而不是從0開始。如圖,常量池容量的16進(jìn)制數(shù)是0X02ED,對應(yīng)十進(jìn)制749,這就代表常量池中有748項常量,索引值范圍為 1-749

常量池中主要存放兩大類常量:字面量和符號引用
訪問標(biāo)志
在常量池結(jié)束之后,緊接著的兩個字節(jié)代表訪問標(biāo)志acces_flags,用于標(biāo)識一些類或者接口層次的訪問信息,包括這個Class是類還是接口,是否定義為public等
類索引,父類索引與接口索引集合
- 類索引:一個U2類型的數(shù)據(jù),用來確定這個類的全限定名
- 父類索引:一個U2類型的數(shù)據(jù),用來確定這個類的父類的全限定名。由于JAVA不允許多重繼承,所以父類索引引用只有一個。
- 接口索引集合:一組U2類型的數(shù)據(jù)集合,用來描述這個類實現(xiàn)了哪些接口
類索引,父類索引與接口索引集合都按順序排列在訪問標(biāo)志之后。
二.類加載機(jī)制
類加載的時機(jī)
類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載七個階段。驗證,準(zhǔn)備,解析3個階段部分統(tǒng)稱為連接。
其中加載、驗證、準(zhǔn)備、初始化、和卸載這5個階段的順序是確定的。
而解析階段不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java的運(yùn)行時綁定。

對于初始化階段,虛擬機(jī)嚴(yán)格規(guī)定有且只有5種情況必須立即對類進(jìn)行初始化
遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
eg:使用new關(guān)鍵字實例化對象的時候;讀取或者設(shè)置一個類的靜態(tài)字段;調(diào)用一個類的靜態(tài)方法時等使用java.lang.reflect包對類進(jìn)行反射調(diào)用的時候
當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有初始化,則要先觸發(fā)其父類的初始化
當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類時
當(dāng)使用JDK 1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)構(gòu)REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
這5種場景中的行為稱為對一個類進(jìn)行主動引用
所有引用類的方式都不會觸發(fā)初始化,稱為被動引用
對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因為通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。
被動引用例子
通過子類引用父類的靜態(tài)字段,不會導(dǎo)致子類初始化
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)中并沒有直接引用定義常量的類,因此不會觸發(fā)定義常量的類的初始化。
加載
虛擬機(jī)需要完成三件事情
通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)(方法區(qū)里用來存儲虛擬機(jī)加載的類信息)
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。這個Class對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口
驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。驗證階段大致會完成下面4個階段的檢驗動作
一.文件格式驗證:是否以魔數(shù)0xCAFEBABE開頭,主次版本是否在處理機(jī)處理范圍內(nèi)等
二.元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進(jìn)行語義分析,以保證描述的信息符合JAVA語言規(guī)范的要求,比如是否有父類等
三.字節(jié)碼驗證:這個階段是最復(fù)雜的一個階段,主要 目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的,符合邏輯的。在第二個階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段將對類的方法體進(jìn)行校驗,保證被校驗類的方法在運(yùn)行時不會做出危害虛擬機(jī)安全的事件。
四.符號引用驗證:最后一個階段的校驗發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)換動作將在連接第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外的信息進(jìn)行匹配性校驗,比如校驗 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類;符號引用中的類,字段,方法的訪問性(public ,private..)是否可以被當(dāng)前訪問等。符號引用驗證的目的是確保解析動作能正常執(zhí)行
準(zhǔn)備
準(zhǔn)備階段是正式為類變量 分配內(nèi)存 并且設(shè)置 類變量初始值 的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。
這里分配內(nèi)存僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
其次這里所說的初始值是通常情況下的數(shù)據(jù)類型零值
//假設(shè)一個類變量定位為
public static int value = 123;
那么變量valuew在準(zhǔn)備階段過后初始值為0而不是123,因為這時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器< clinit >()方法中所以把value賦值為123是在初始化階段才會執(zhí)行。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的 符號引用 替換為 直接引用 的過程。
符號引用:以一組符號來描述所引用的目標(biāo),符號可以使任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)中即可。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。
直接引用:可以是直接指向目標(biāo)的指針,相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用,那么引用的目標(biāo)必定存在內(nèi)存中。
初始化
類初始化時類加載的最后一步,前面類加載過程中,除了加載階段用戶可以通過自定義類加載器參與以外,其余動作都是虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才是真正執(zhí)行類中定義Java程序代碼。
準(zhǔn)備階段中,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,根據(jù)程序員通過程序制定的主觀計劃初始化類變量。初始化過程其實是執(zhí)行類構(gòu)造器< clinit >()方法的過程。
< clinit >()方法是由編譯器自動收集類中 所有類變量的賦值動作 和 靜態(tài)語句塊 中的語句合并產(chǎn)生的。收集的順序是按照語句在源文件中出現(xiàn)的順序。靜態(tài)語句塊中只能訪問定義在靜態(tài)語句塊之前的變量,定義在它之后的變量可以賦值,但不能訪問。
public class Test{
static{
i = 0; //給變量賦值,可以通過編譯
System.out.print(i); //這句編譯器會提示“非法向前引用”
}
static int i = 1;
}
< clinit >()方法與類構(gòu)造函數(shù)(或者說實例構(gòu)造器< init >())不同,他不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會保證子類的< clinit >()方法執(zhí)行之前,父類的< clinit >()已經(jīng)執(zhí)行完畢。
類加載器
通過一個類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流。
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。
比較兩個類是否相等,只有在這兩個類都是同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機(jī)加載,只要加載他們的類加載器不同,那么這兩個類就必定不相等。
從JAVA虛擬機(jī)角度講,只存在兩種不同的類加載器:
一種是啟動類加載器,這個類加載器使用C++語言實現(xiàn),是虛擬機(jī)自身的一部分
另一種就是所有其他的類加載器,這些類加載器使用JAVA語言實現(xiàn),獨立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader
雙親委派模型
絕大部分JAVA程序都會使用到3種系統(tǒng)提供的類加載器。
啟動類加載器,擴(kuò)展類加載器,應(yīng)用程序加載器。
類加載器雙親委派模型為,相互之間為組合關(guān)系

工作過程:如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求時,子加載器才會去嘗試自己去加載。