文章作為《深入理解Java虛擬機》讀書筆記,講的可能就沒書本詳細。
一.類文件結(jié)構(gòu)
Java虛擬機多平臺
都是統(tǒng)一使用的程序存儲格式——字節(jié)碼.class文件
任何語言都可以被特定的編譯器編譯為存儲字節(jié)碼的Class文件币绩,Class文件中包含了Java虛擬機指令集和符號表以及若干其他輔助信息参滴。虛擬機并不關(guān)心Class的來源是何種語言。
Class文件結(jié)構(gòu)
Class文件是一組以8位字節(jié)為基礎(chǔ)單位二進制流峦树,各個數(shù)據(jù)項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符旦事。
任何一個Class文件都對應著唯一一個類或接口的定義信息魁巩,但反過來說,類或接口并不一定都得定義在文件里族檬,類或接口也可以通過類加載器直接生成歪赢。
Class文件格式采用一種類似C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)據(jù),包含兩種數(shù)據(jù)類型:無符號數(shù)和表
魔術(shù)與Class文件的版本
使用十六進制編譯器WinHex打開任意一個class文件单料,可以看到它的結(jié)構(gòu)埋凯。
前4個字節(jié)0-3表示為魔數(shù):唯一作用是確定這個文件是否為一個能被虛擬機接受的Class文件,值為0XCAFEBABE扫尖,如圖中所示
接著兩位是次版本號4-5白对,這里值為0x0000
接著兩位是主版本號6-7,這里值為0X0033换怖,也就是十進制51甩恼,表明當前JDK版本號在1.7以上。
常量池
緊接著主次版本號之后的是常量池入口沉颂,它是占用Class文件空間最大的數(shù)據(jù)項目之一条摸,同時還是在Class文件中第一個出現(xiàn)的表類型數(shù)據(jù)項目。
由于常量池中常量的數(shù)目是不固定的铸屉,所以在入口需要放置一項U2類型2個字節(jié)的數(shù)據(jù)代表常量池容量計數(shù)值钉蒲。
這個容量計數(shù)是從1開始而不是從0開始。如圖彻坛,常量池容量的16進制數(shù)是0X02ED顷啼,對應十進制749,這就代表常量池中有748項常量昌屉,索引值范圍為 1-749
常量池中主要存放兩大類常量:字面量和符號引用
訪問標志
在常量池結(jié)束之后钙蒙,緊接著的兩個字節(jié)代表訪問標志acces_flags,用于標識一些類或者接口層次的訪問信息间驮,包括這個Class是類還是接口躬厌,是否定義為public等
類索引,父類索引與接口索引集合
- 類索引:一個U2類型的數(shù)據(jù)竞帽,用來確定這個類的全限定名
- 父類索引:一個U2類型的數(shù)據(jù)烤咧,用來確定這個類的父類的全限定名偏陪。由于JAVA不允許多重繼承,所以父類索引引用只有一個煮嫌。
- 接口索引集合:一組U2類型的數(shù)據(jù)集合笛谦,用來描述這個類實現(xiàn)了哪些接口
類索引,父類索引與接口索引集合都按順序排列在訪問標志之后昌阿。
二.類加載機制
類加載的時機
類從被加載到虛擬機內(nèi)存開始饥脑,到卸載出內(nèi)存為止,整個生命周期包括:加載懦冰、驗證灶轰、準備、解析刷钢、初始化笋颤、使用和卸載七個階段。驗證内地,準備伴澄,解析3個階段部分統(tǒng)稱為連接。
其中加載阱缓、驗證非凌、準備、初始化荆针、和卸載這5個階段的順序是確定的敞嗡。
而解析階段不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java的運行時綁定航背。
對于初始化階段喉悴,虛擬機嚴格規(guī)定有且只有5種情況必須立即對類進行初始化
遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化玖媚,則需要先觸發(fā)其初始化箕肃。
eg:使用new關(guān)鍵字實例化對象的時候;讀取或者設(shè)置一個類的靜態(tài)字段最盅;調(diào)用一個類的靜態(tài)方法時等使用java.lang.reflect包對類進行反射調(diào)用的時候
當初始化一個類的時候突雪,如果發(fā)現(xiàn)其父類還沒有初始化起惕,則要先觸發(fā)其父類的初始化
當虛擬機啟動時涡贱,用戶需要指定一個要執(zhí)行的主類時
當使用JDK 1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)構(gòu)REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄惹想,并且這個方法句柄所對應的類沒有進行過初始化问词,則需要先觸發(fā)其初始化。
這5種場景中的行為稱為對一個類進行主動引用
所有引用類的方式都不會觸發(fā)初始化嘀粱,稱為被動引用
對于靜態(tài)字段激挪,只有直接定義這個字段的類才會被初始化辰狡,因為通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化垄分。
被動引用例子
通過子類引用父類的靜態(tài)字段宛篇,不會導致子類初始化
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
常量在編譯階段會存入調(diào)用類的常量池中薄湿,本質(zhì)中并沒有直接引用定義常量的類叫倍,因此不會觸發(fā)定義常量的類的初始化。
加載
虛擬機需要完成三件事情
通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)(方法區(qū)里用來存儲虛擬機加載的類信息)
在內(nèi)存中生成一個代表這個類的java.lang.Class對象豺瘤,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口吆倦。這個Class對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口
驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求坐求,并且不會危害虛擬機自身的安全蚕泽。驗證階段大致會完成下面4個階段的檢驗動作
一.文件格式驗證:是否以魔數(shù)0xCAFEBABE開頭,主次版本是否在處理機處理范圍內(nèi)等
二.元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析桥嗤,以保證描述的信息符合JAVA語言規(guī)范的要求须妻,比如是否有父類等
三.字節(jié)碼驗證:這個階段是最復雜的一個階段,主要 目的是通過數(shù)據(jù)流和控制流分析砸逊,確定程序語義是合法的璧南,符合邏輯的。在第二個階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后师逸,這個階段將對類的方法體進行校驗司倚,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。
四.符號引用驗證:最后一個階段的校驗發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候篓像,這個轉(zhuǎn)換動作將在連接第三階段——解析階段中發(fā)生动知。符號引用驗證可以看作是對類自身以外的信息進行匹配性校驗,比如校驗 符號引用中通過字符串描述的全限定名是否能找到對應的類员辩;符號引用中的類盒粮,字段,方法的訪問性(public ,private..)是否可以被當前訪問等奠滑。符號引用驗證的目的是確保解析動作能正常執(zhí)行
準備
準備階段是正式為類變量 分配內(nèi)存 并且設(shè)置 類變量初始值 的階段丹皱,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。
這里分配內(nèi)存僅包括類變量(被static修飾的變量)宋税,而不包括實例變量摊崭,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
其次這里所說的初始值是通常情況下的數(shù)據(jù)類型零值
//假設(shè)一個類變量定位為
public static int value = 123;
那么變量valuew在準備階段過后初始值為0而不是123杰赛,因為這時候尚未開始執(zhí)行任何Java方法呢簸,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器< clinit >()方法中所以把value賦值為123是在初始化階段才會執(zhí)行。
解析
解析階段是虛擬機將常量池內(nèi)的 符號引用 替換為 直接引用 的過程根时。
符號引用:以一組符號來描述所引用的目標瘦赫,符號可以使任何形式的字面量,只要使用時能無歧義地定位到目標中即可蛤迎。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)确虱,引用的目標并不一定已經(jīng)加載到內(nèi)存中。
直接引用:可以是直接指向目標的指針替裆,相對偏移量或是一個能間接定位到目標的句柄蝉娜。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用扎唾,那么引用的目標必定存在內(nèi)存中召川。
初始化
類初始化時類加載的最后一步,前面類加載過程中胸遇,除了加載階段用戶可以通過自定義類加載器參與以外荧呐,其余動作都是虛擬機主導和控制。到了初始化階段纸镊,才是真正執(zhí)行類中定義Java程序代碼倍阐。
準備階段中,變量已經(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)造器,虛擬機會保證子類的< clinit >()方法執(zhí)行之前计螺,父類的< clinit >()已經(jīng)執(zhí)行完畢夯尽。
類加載器
通過一個類的全限定名來獲取描述此類的二進制字節(jié)流。
對于任意一個類登馒,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性匙握,每一個類加載器,都擁有一個獨立的類名稱空間谊娇。
比較兩個類是否相等肺孤,只有在這兩個類都是同一個類加載器加載的前提下才有意義,否則济欢,即使這兩個類來源于同一個Class文件赠堵,被同一個虛擬機加載,只要加載他們的類加載器不同法褥,那么這兩個類就必定不相等茫叭。
從JAVA虛擬機角度講,只存在兩種不同的類加載器:
一種是啟動類加載器半等,這個類加載器使用C++語言實現(xiàn)揍愁,是虛擬機自身的一部分
另一種就是所有其他的類加載器,這些類加載器使用JAVA語言實現(xiàn)杀饵,獨立于虛擬機外部莽囤,并且全都繼承自抽象類java.lang.ClassLoader
雙親委派模型
絕大部分JAVA程序都會使用到3種系統(tǒng)提供的類加載器。
啟動類加載器切距,擴展類加載器辽剧,應用程序加載器竭翠。
類加載器雙親委派模型為,相互之間為組合關(guān)系
工作過程:如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類谒出,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此氓仲,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中朗恳,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會去嘗試自己去加載蔚叨。