聲明:本文摘抄自《深入理解Java虛擬機(jī)》一書(shū),本文完全為自我學(xué)習(xí),請(qǐng)感興趣的同學(xué)購(gòu)買(mǎi)正版,支持原創(chuàng)
加載
“加載”是“類加載”過(guò)程的一個(gè)階段,在加載階段,虛擬機(jī)需要完成以下3件事情:
- 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。
虛擬機(jī)規(guī)范的這3條并不算具體,例如“通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流”這條,它沒(méi)有指明二進(jìn)制字節(jié)流要從一個(gè)Class文件中獲取,準(zhǔn)確地說(shuō)是根本沒(méi)有指明要從哪里獲取,怎樣獲取。因此二進(jìn)制字節(jié)流可以通過(guò)多種方式獲取,例如:
- 從ZIP包中讀取,如JAR,EAR,WAR格式文件。
- 從網(wǎng)絡(luò)中獲取,典型場(chǎng)景是Applet應(yīng)用。
- 運(yùn)行時(shí)計(jì)算生成,這種場(chǎng)景使用最多的就是動(dòng)態(tài)代理技術(shù)。
- 由其他文件生成,典型場(chǎng)景是JSP應(yīng)用。
- 從數(shù)據(jù)庫(kù)中讀取,這種場(chǎng)景相對(duì)少見(jiàn),例如有些中間件服務(wù)器(SAP Netweaver)可以選擇把程序安裝到數(shù)據(jù)庫(kù)中來(lái)完成代碼在集群間的分發(fā)。
相對(duì)類加載過(guò)程的其他階段,加載階段是開(kāi)發(fā)人員可控性最強(qiáng)的,因?yàn)榧虞d階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成,開(kāi)發(fā)人員可以通過(guò)自己的類加載器去控制字節(jié)流的獲取方式。
加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需要的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)規(guī)范中并未明確規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象(并沒(méi)有規(guī)定是在Java堆中,對(duì)于HotSpot虛擬機(jī)而言,Class對(duì)象比較特殊,它雖然是對(duì)象,但是存放在方法區(qū)里面),這個(gè)對(duì)象作為程序訪問(wèn)方法區(qū)中的這些類型數(shù)據(jù)的訪問(wèn)入口。
加載階段和連接階段的部分內(nèi)容是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開(kāi)始。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class的二進(jìn)制字節(jié)流中包含的信息符合虛擬機(jī)規(guī)范的要求,并且不會(huì)危害到虛擬機(jī)自身的安全。
驗(yàn)證階段大致上會(huì)完成以下4個(gè)動(dòng)作:
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證
- 字節(jié)碼驗(yàn)證
- 符號(hào)引用驗(yàn)證
1. 文件格式驗(yàn)證
文件格式驗(yàn)證的包括但不限于以下幾點(diǎn):
- 是否以魔數(shù)0xCAFEBABE開(kāi)頭
- 主,次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍內(nèi)
- 常量池的常量中是否有不被支持的常量類型(檢查常量的tag標(biāo)志)
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)
- Class文件各個(gè)部分及文件是否有被刪除的或附件的其他信息
......
這個(gè)階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ),所以后面3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
2. 元數(shù)據(jù)驗(yàn)證
第二階段驗(yàn)證的主要目的是對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn)??赡馨ǖ尿?yàn)證點(diǎn)如下:
- 這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)該有父類)
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類,如String)
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法。
- 類中的方法,字段是否與父類產(chǎn)生沖突(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類型卻不同等)。
......
3. 字節(jié)碼驗(yàn)證
字節(jié)碼驗(yàn)證是整個(gè)驗(yàn)證階段中最復(fù)雜的一個(gè),主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是否是合法的,符合邏輯的。
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類似這樣的情況:在操作棧上放置了一個(gè)int類型的數(shù)據(jù),使用時(shí)確按long類型來(lái)加載入本地變量表中。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。
- 保證方法體中類型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,這是安全的,但是把父類對(duì)象賦值給子類數(shù)據(jù)類型,甚至把對(duì)象賦值給與它毫無(wú)繼承關(guān)系,完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的。
......
4. 符號(hào)引用驗(yàn)證
符號(hào)引用驗(yàn)證階段發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在解析階段中發(fā)生。符號(hào)引用驗(yàn)證可以看做是對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn),通常校驗(yàn)如下的內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能夠找到對(duì)應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段。
- 符號(hào)引用中的類,字段,方法是否可以被當(dāng)前類所訪問(wèn)。
......
符號(hào)引用驗(yàn)證的主要目的是保證解析動(dòng)作能夠正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,那么將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError異常的子類,例如java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError等。
對(duì)于虛擬機(jī)的類加載機(jī)制而言,驗(yàn)證階段是非常重要的,但不是一定必要的階段。如果所運(yùn)行的全部代碼(包括自己編寫(xiě)的以及第三方包中的代碼)都已經(jīng)被反復(fù)使用和驗(yàn)證過(guò),那么在實(shí)施階段就可以考慮使用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載時(shí)間。
準(zhǔn)備
準(zhǔn)備階段是為類變量分配內(nèi)存并設(shè)置類變量初始值階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段有兩個(gè)概念容易產(chǎn)生混淆,首先,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其次,這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量定義為:
public static int value = 123;
那變量value在準(zhǔn)備階段過(guò)后初始值是0而不是123,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是在程序被編譯后,存放與類構(gòu)造器<cinit>()方法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。

上面提到,在“通常情況”下初始值是零值,那相對(duì)會(huì)有一些“特殊情況”:如果類字段的字段屬性中存在ConstantValue屬性,那么在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值,假設(shè)上面類變量value的定義為:
public static final int value = 123;
編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。
解析
解析階段是虛擬機(jī)將常量池中的符號(hào)引用替換為直接引用的過(guò)程。
符號(hào)引用(Symbolic Reference):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)內(nèi)存布局無(wú)關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中,
直接引用(Direct Reference):直接引用可以是直接指向目標(biāo)的指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)內(nèi)存布局相關(guān)的。如果有了直接引用,那么引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
虛擬機(jī)規(guī)范中并未規(guī)定解析階段發(fā)生的具體時(shí)間,只要求了在執(zhí)行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield和putstatic這16個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們所使用的符號(hào)引用進(jìn)行解析。
對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見(jiàn)的事情,除invokedynamic指令以外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析結(jié)果進(jìn)行緩存(在運(yùn)行時(shí)常量池中記錄直接引用,并把常量標(biāo)示為已解析狀態(tài)),從而避免解析動(dòng)作重復(fù)進(jìn)行。
對(duì)于invokedynamic指令,該指令是用于動(dòng)態(tài)語(yǔ)音支持的,它所對(duì)應(yīng)的引用稱為“動(dòng)態(tài)調(diào)用點(diǎn)限定符(Dynamic Call Site Specifier)”,這里“動(dòng)態(tài)”的含義是指必須等到程序運(yùn)行到這條指令的時(shí)候,解析動(dòng)作才能進(jìn)行。相對(duì)的,其余可觸發(fā)解析的指令都是“靜態(tài)”的,可以在剛剛完成加載階段,還沒(méi)有開(kāi)始執(zhí)行代碼時(shí)就進(jìn)行解析。
1. 類或接口解析
假設(shè)當(dāng)前代碼所處的類為D,如果要把一個(gè)從未解析過(guò)的符號(hào)引用N解析為一個(gè)類或接口C的直接引用,那虛擬機(jī)完成整個(gè)解析過(guò)程需要以下3個(gè)步驟:
- 如果C不是一個(gè)數(shù)組類型,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給D的類加載器去加載這個(gè)類C。在加載過(guò)程中,由于元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證的需要,又可能觸發(fā)其他相關(guān)類的加載動(dòng)作,例如加載這個(gè)類的父類或?qū)崿F(xiàn)的接口。一旦這個(gè)加載過(guò)程出現(xiàn)了任何異常,解析過(guò)程就宣告失敗。
- 如果C是一個(gè)數(shù)組類型,并且數(shù)組的元素類型為對(duì)象,也就是N的描述符會(huì)是類似“[Ljava/lang/Integer”的形式,那將會(huì)按照第1點(diǎn)的規(guī)則加載數(shù)組元素類型。如果N的描述符如前面所假設(shè)的形式,需要加載的元素類型就是“java.lang.Integer”,接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象。
- 如果上面的步驟沒(méi)有出現(xiàn)任何異常,那么C在虛擬機(jī)中實(shí)際上已經(jīng)是一個(gè)有效的類或接口了,但在解析完成之前還要進(jìn)行符號(hào)引用驗(yàn)證,確認(rèn)D是否具備對(duì)C的訪問(wèn)權(quán)限。如果發(fā)現(xiàn)不具備訪問(wèn)權(quán)限,將拋出java.lang.IllegalAccessError異常。
2. 字段解析
要解析一個(gè)未被解析過(guò)的字段符號(hào)引用,首先將會(huì)對(duì)字段表內(nèi)class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析,也就是字段所屬的類或接口的符號(hào)引用。如果解析成功完成,那將這個(gè)字段所屬的類或接口用C表示,虛擬機(jī)規(guī)范要求按照如下步驟對(duì)C進(jìn)行后續(xù)字段的搜索:
- 如果C本身就包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
- 否則,如果在C中實(shí)現(xiàn)了接口,將會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口,如果接口中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
- 否則,如果C不是java.lang.Object的話,將會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類,如果在父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
- 如果查找成功,將會(huì)對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證,如果發(fā)現(xiàn)不具備對(duì)字段的訪問(wèn)權(quán)限,則會(huì)拋出java.lang.IllegalAccessError。如果查找失敗,拋出java.lang.NoSuchFieldError異常。
在實(shí)際應(yīng)用中,虛擬機(jī)的編譯器實(shí)現(xiàn)可能比上述規(guī)范要求的更為嚴(yán)格,如果有一個(gè)同名字段同時(shí)出現(xiàn)在C的接口和父接口中,或者同時(shí)出現(xiàn)在自己或者父類的多個(gè)接口中,那么編譯器將可能拒絕編譯。
3. 類方法解析
略
4. 接口方法解析
略
初始化
初始化階段是執(zhí)行類構(gòu)造器<cinit>()方法的過(guò)程。
- <cinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是按照語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句塊可以進(jìn)行賦值,但不能訪問(wèn)。
- <cinit>()方法與類構(gòu)造函數(shù)(實(shí)例構(gòu)造器<init>()方法)不同,它不需要顯示的調(diào)用父類的構(gòu)造器,虛擬機(jī)會(huì)保證在子類的<cinit>()方法執(zhí)行之前,父類的<cinit>()方法已經(jīng)執(zhí)行完畢。因此Java虛擬機(jī)中第一個(gè)被執(zhí)行的<cinit>()方法的類肯定是java.lang.Object。
- 由于父類的<cinit>()方法先執(zhí)行,也意味著父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類的變量賦值操作。
- <cinit>()方法對(duì)于類或接口來(lái)說(shuō)不是必須的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不為這個(gè)類生成<cinit>()方法。
- 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有變量初始化的賦值操作,因此接口和類一樣都會(huì)生成<cinit>()方法。但接口與類不同的是,執(zhí)行接口的<cinit>()方法,不需要先執(zhí)行父接口的<cinit>()方法,只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口實(shí)現(xiàn)的類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<cinit>()方法。
- 虛擬機(jī)會(huì)保證一個(gè)類的<cinit>()方法在多線程環(huán)境下被正確的加鎖和同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<cinit>()方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<cinit>()方法結(jié)束。如果<cinit>()方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。