虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存渗饮,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)
、轉(zhuǎn)換解析
和初始化
宿刮,最終形成可以被虛擬機(jī)直接使用的Java類(lèi)型互站,這就是虛擬機(jī)的類(lèi)加載機(jī)制。
與那些在編譯時(shí)需要進(jìn)行連接工作的語(yǔ)言不同糙置,在Java語(yǔ)言里面云茸,類(lèi)型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的谤饭,這種策略雖然會(huì)令類(lèi)加載時(shí)稍微增加一些性能開(kāi)銷(xiāo)标捺,但是會(huì)為Java應(yīng)用程序提供高度的靈活性
類(lèi)加載的過(guò)程:
類(lèi)從被加載到內(nèi)存中開(kāi)始懊纳,到卸載出內(nèi)存,經(jīng)歷了
加載
亡容、連接
嗤疯、初始化
、使用
四個(gè)階段闺兢,其中連接又包含了驗(yàn)證
茂缚、準(zhǔn)備
、解析
三個(gè)步驟屋谭。這些步驟總體上是按照?qǐng)D中順序進(jìn)行的脚囊,但是Java語(yǔ)言本身支持運(yùn)行時(shí)綁定,所以解析階段也可以是在初始化之后進(jìn)行的桐磁。以上順序都只是說(shuō)開(kāi)始的順序悔耘,實(shí)際過(guò)程中是交叉進(jìn)行的,加載過(guò)程中可能就已經(jīng)開(kāi)始驗(yàn)證了我擂。
類(lèi)的加載時(shí)機(jī)
什么情況下需要開(kāi)始類(lèi)加載過(guò)程的第一個(gè)階段:加載
衬以,Java虛擬機(jī)規(guī)范中并沒(méi)有進(jìn)行強(qiáng)制約束。但是對(duì)于初始化階段校摩,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(而加載看峻、驗(yàn)證、準(zhǔn)備自然需要在此之前開(kāi)始):
- 遇到
new
衙吩、getstatic
互妓、putstatic
或invokestatic
這4條字節(jié)碼指令時(shí),如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化坤塞,則需要先觸發(fā)其初始化车猬。生成這4條指令的最常見(jiàn)的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(被final修飾尺锚、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法的時(shí)候惜浅。 - 使用
java.lang.reflect
包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候瘫辩,如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化坛悉。 - 當(dāng)初始化一個(gè)類(lèi)的時(shí)候伐厌,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化裸影。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)挣轨,用戶需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)轩猩。
- 當(dāng)使用JDK 1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)卷扮,如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic荡澎、REF_putStatic、REF_invokeStatic的方法句柄晤锹,并且這個(gè)方法句柄所對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化摩幔,則需要先觸發(fā)其初始化。
對(duì)于這5種會(huì)觸發(fā)類(lèi)進(jìn)行初始化的場(chǎng)景鞭铆,虛擬機(jī)規(guī)范中使用了一個(gè)很強(qiáng)烈的限定語(yǔ):“有且只有”或衡,這5種場(chǎng)景中的行為稱(chēng)為對(duì)一個(gè)類(lèi)進(jìn)行主動(dòng)引用
。除此之外车遂,所有引用類(lèi)的方式都不會(huì)觸發(fā)初始化封断,稱(chēng)為被動(dòng)引用
。
被動(dòng)引用的例子一
/**
* 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段舶担,不會(huì)導(dǎo)致子類(lèi)初始化
*/
public class SuperClass {
static {
System.out.println("SuperClass init坡疼!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主動(dòng)使用類(lèi)字段
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
上述代碼運(yùn)行之后柄沮,只會(huì)輸出"SuperClass init回梧!",而不會(huì)輸出"SubClass init祖搓!"狱意。對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化
拯欧,因此通過(guò)其子類(lèi)來(lái)引用父類(lèi)中定義的靜態(tài)字段详囤,只會(huì)觸發(fā)父類(lèi)的初始化而不會(huì)觸發(fā)子類(lèi)的初始化。至于是否要觸發(fā)子類(lèi)的加載和驗(yàn)證镐作,在虛擬機(jī)規(guī)范中并未明確規(guī)定藏姐,這點(diǎn)取決于虛擬機(jī)的具體實(shí)現(xiàn)。
被動(dòng)引用的例子二
/**
* 通過(guò)數(shù)組定義來(lái)引用類(lèi)该贾,不會(huì)觸發(fā)此類(lèi)的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
并沒(méi)有觸發(fā)類(lèi)org.fenixsoft.classloading.SuperClass的初始化階段羔杨。但是這段代碼里面觸發(fā)了另外一個(gè)名為[Lorg.fenixsoft.classloading.SuperClass
的類(lèi)的初始化階段;這個(gè)類(lèi)代表了一個(gè)元素類(lèi)型為org.fenixsoft.classloading.SuperClass的一維數(shù)組杨蛋,數(shù)組中應(yīng)有的屬性和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實(shí)現(xiàn)在這個(gè)類(lèi)里兜材。
被動(dòng)引用的例子三
/**
* 常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi)逞力,因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化曙寡。
*/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主動(dòng)使用類(lèi)字段演示
*/
public class NotInitialization {
public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
一寇荧、加載
加載
是“類(lèi)加載”(Class Loading)過(guò)程的一個(gè)階段举庶。在加載階段,虛擬機(jī)需要完成以下3件事情:
- 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(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è)類(lèi)的java.lang.Class對(duì)象镀琉,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口。
通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流
這條添祸,它沒(méi)有指明二進(jìn)制字節(jié)流要從一個(gè)Class文件中獲取滚粟,準(zhǔn)確地說(shuō)是根本沒(méi)有指明要從哪里獲取、怎樣獲取刃泌。許多舉足輕重的Java技術(shù)都建立在這一基礎(chǔ)之上凡壤,例如:
- 從ZIP包中讀取,這很常見(jiàn)耙替,最終成為日后JAR亚侠、EAR、WAR格式的基礎(chǔ)俗扇。
- 從網(wǎng)絡(luò)中獲取硝烂,這種場(chǎng)景最典型的應(yīng)用就是Applet。
- 運(yùn)行時(shí)計(jì)算生成铜幽,這種場(chǎng)景使用得最多的就是動(dòng)態(tài)代理技術(shù)滞谢,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來(lái)為特定接口生成形式為"*$Proxy"的代理類(lèi)的二進(jìn)制字節(jié)流除抛。
相對(duì)于類(lèi)加載過(guò)程的其他階段狮杨,一個(gè)非數(shù)組類(lèi)的加載階段(準(zhǔn)確地說(shuō),是加載階段中獲取類(lèi)的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的到忽,因?yàn)榧虞d階段既可以使用系統(tǒng)提供的引導(dǎo)類(lèi)加載器來(lái)完成橄教,也可以由用戶自定義的類(lèi)加載器去完成,開(kāi)發(fā)人員可以通過(guò)定義自己的類(lèi)加載器去控制字節(jié)流的獲取方式(即重寫(xiě)一個(gè)類(lèi)加載器的loadClass()方法)喘漏。
對(duì)于數(shù)組類(lèi)而言护蝶,情況就有所不同,數(shù)組類(lèi)本身不通過(guò)類(lèi)加載器創(chuàng)建翩迈,它是由Java虛擬機(jī)直接創(chuàng)建的持灰。但數(shù)組類(lèi)與類(lèi)加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類(lèi)的元素類(lèi)型(Element Type负饲,指的是數(shù)組去掉所有維度的類(lèi)型)最終是要靠類(lèi)加載器去創(chuàng)建的
二搅方、驗(yàn)證
這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全绽族。
驗(yàn)證階段是非常重要的,這個(gè)階段是否嚴(yán)謹(jǐn)衩藤,直接決定了Java虛擬機(jī)是否能承受惡意代碼的攻擊吧慢,從執(zhí)行性能的角度上講,驗(yàn)證階段的工作量在虛擬機(jī)的類(lèi)加載子系統(tǒng)中又占了相當(dāng)大的一部分赏表。如果驗(yàn)證到輸入的字節(jié)流不符合Class文件格式的約束检诗,虛擬機(jī)就應(yīng)拋出一個(gè)java.lang.VerifyError異承僬蹋或其子類(lèi)異常。
驗(yàn)證階段大致上會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證
逢慌、元數(shù)據(jù)驗(yàn)證
悠轩、字節(jié)碼驗(yàn)證
、符號(hào)引用驗(yàn)證
:
文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范攻泼,并且能被當(dāng)前版本的虛擬機(jī)處理火架。這一階段可能包括下面這些驗(yàn)證點(diǎn):
- 是否以魔數(shù)0xCAFEBABE開(kāi)頭
- 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)忙菠。
- 常量池的常量中是否有不被支持的常量類(lèi)型(檢查常量tag標(biāo)志)何鸡。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類(lèi)型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)牛欢。
- Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息骡男。
...
該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類(lèi)型信息的要求傍睹。
元數(shù)據(jù)驗(yàn)證
第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析隔盛,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:
- 這個(gè)類(lèi)是否有父類(lèi)(除了java.lang.Object之外拾稳,所有的類(lèi)都應(yīng)當(dāng)有父類(lèi))吮炕。
- 這個(gè)類(lèi)的父類(lèi)是否繼承了不允許被繼承的類(lèi)(被final修飾的類(lèi))。
- 如果這個(gè)類(lèi)不是抽象類(lèi)熊赖,是否實(shí)現(xiàn)了其父類(lèi)或接口之中要求實(shí)現(xiàn)的所有方法来屠。
- 類(lèi)中的字段、方法是否與父類(lèi)產(chǎn)生矛盾(例如覆蓋了父類(lèi)的final字段震鹉,或者出現(xiàn)不符合規(guī)則的方法重載俱笛,例如方法參數(shù)都一致,但返回值類(lèi)型卻不同等)传趾。
……
第二階段的主要目的是對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn)迎膜,保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息。
字節(jié)碼驗(yàn)證
第三階段是整個(gè)驗(yàn)證過(guò)程中最復(fù)雜的一個(gè)階段浆兰,主要目的是通過(guò)數(shù)據(jù)流和控制流分析磕仅,確定程序語(yǔ)義是合法的、符合邏輯的簸呈。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類(lèi)型做完校驗(yàn)后榕订,這個(gè)階段將對(duì)類(lèi)的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件蜕便,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列都能配合工作劫恒,例如不會(huì)出現(xiàn)類(lèi)似這樣的情況:在操作棧放置了一個(gè)int類(lèi)型的數(shù)據(jù),使用時(shí)卻按long類(lèi)型來(lái)加載入本地變量表中。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上两嘴。
- 保證方法體中的類(lèi)型轉(zhuǎn)換是有效的丛楚,例如可以把一個(gè)子類(lèi)對(duì)象賦值給父類(lèi)數(shù)據(jù)類(lèi)型,這是安全的憔辫,但是把父類(lèi)對(duì)象賦值給子類(lèi)數(shù)據(jù)類(lèi)型趣些,甚至把對(duì)象賦值給與它毫無(wú)繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類(lèi)型贰您,則是危險(xiǎn)和不合法的坏平。
……
符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生枉圃。符號(hào)引用驗(yàn)證可以看做是對(duì)類(lèi)自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)功茴,通常需要校驗(yàn)下列內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類(lèi)。
- 在指定類(lèi)中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱(chēng)所描述的方法和字段孽亲。
- 符號(hào)引用中的類(lèi)坎穿、字段、方法的訪問(wèn)性(private返劲、protected玲昧、public、default)是否可被當(dāng)前類(lèi)訪問(wèn)篮绿。
……
符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行孵延,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,那么將會(huì)拋出一個(gè)java.lang.IncompatibleClassChangeError異常的子類(lèi)亲配,如java.lang.IllegalAccessError尘应、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等吼虎。
三犬钢、準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配思灰。
這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量(被static修飾的變量)玷犹,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中洒疚。其次歹颓,這里所說(shuō)的初始值通常情況
下是數(shù)據(jù)類(lèi)型的零值,假設(shè)一個(gè)類(lèi)變量的定義為:
public static int value = 123;
value在準(zhǔn)備階段過(guò)后的初始值為0而不是123油湖,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何Java方法巍扛,而把value賦值為123的putstatic指令是程序被編譯后,存放于類(lèi)構(gòu)造器<clinit>()方法之中乏德,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行撤奸。
相對(duì)的會(huì)有一些特殊情況
:如果類(lèi)字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值,假設(shè)上面類(lèi)變量value的定義變?yōu)椋?/p>
public static final int value = 123;
編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性寂呛,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。
四瘾晃、解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程
-
符號(hào)引用(Symbolic References)
:符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)贷痪,符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可蹦误。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān)劫拢,引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同强胰,但是它們能接受的符號(hào)引用必須都是一致的舱沧,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中。 -
直接引用(Direct References)
:直接引用可以是直接指向目標(biāo)的指針偶洋、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄熟吏。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同玄窝。如果有了直接引用牵寺,那引用的目標(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)行解析藤树。
解析動(dòng)作主要針對(duì)類(lèi)或接口、字段拓萌、類(lèi)方法岁钓、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符7類(lèi)符號(hào)引用進(jìn)行
類(lèi)或接口的解析
假設(shè)當(dāng)前代碼所處的類(lèi)為D屡限,如果要把一個(gè)從未解析過(guò)的符號(hào)引用N解析為一個(gè)類(lèi)或接口C的直接引用品嚣,那虛擬機(jī)完成整個(gè)解析的過(guò)程需要以下3個(gè)步驟:
1)如果C不是一個(gè)數(shù)組類(lèi)型,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給D的類(lèi)加載器去加載這個(gè)類(lèi)C钧大。在加載過(guò)程中翰撑,由于元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證的需要啊央,又可能觸發(fā)其他相關(guān)類(lèi)的加載動(dòng)作眶诈,例如加載這個(gè)類(lèi)的父類(lèi)或?qū)崿F(xiàn)的接口。一旦這個(gè)加載過(guò)程出現(xiàn)了任何異常瓜饥,解析過(guò)程就宣告失敗逝撬。
2)如果C是一個(gè)數(shù)組類(lèi)型,并且數(shù)組的元素類(lèi)型為對(duì)象乓土,也就是N的描述符會(huì)是類(lèi)似"[Ljava/lang/Integer"的形式宪潮,那將會(huì)按照第1點(diǎn)的規(guī)則加載數(shù)組元素類(lèi)型。如果N的描述符如前面所假設(shè)的形式帐我,需要加載的元素類(lèi)型就是"java.lang.Integer"坎炼,接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象。
3)如果上面的步驟沒(méi)有出現(xiàn)任何異常拦键,那么C在虛擬機(jī)中實(shí)際上已經(jīng)成為一個(gè)有效的類(lèi)或接口了谣光,但在解析完成之前還要進(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異常。
字段解析
要解析一個(gè)未被解析過(guò)的字段符號(hào)引用媚朦,首先將會(huì)對(duì)字段表內(nèi)class_index[2]項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析氧敢,也就是字段所屬的類(lèi)或接口的符號(hào)引用。如果在解析這個(gè)類(lèi)或接口符號(hào)引用的過(guò)程中出現(xiàn)了任何異常询张,都會(huì)導(dǎo)致字段符號(hào)引用解析的失敗孙乖。如果解析成功完成,那將這個(gè)字段所屬的類(lèi)或接口用C表示份氧,虛擬機(jī)規(guī)范要求按照如下步驟對(duì)C進(jìn)行后續(xù)字段的搜索唯袄。
1)如果C本身就包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用蜗帜,查找結(jié)束恋拷。
2)否則,如果在C中實(shí)現(xiàn)了接口厅缺,將會(huì)按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口蔬顾,如果接口中包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段宴偿,則返回這個(gè)字段的直接引用,查找結(jié)束诀豁。
3)否則窄刘,如果C不是java.lang.Object的話,將會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類(lèi)舷胜,如果在父類(lèi)中包含了簡(jiǎn)單名稱(chēng)和字段描述符都與目標(biāo)相匹配的字段都哭,則返回這個(gè)字段的直接引用,查找結(jié)束逞带。
4)否則,查找失敗纱新,拋出java.lang.NoSuchFieldError異常展氓。
如果查找過(guò)程成功返回了引用,將會(huì)對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證脸爱,如果發(fā)現(xiàn)不具備對(duì)字段的訪問(wèn)權(quán)限遇汞,將拋出java.lang.IllegalAccessError異常。
3.類(lèi)方法解析
類(lèi)方法解析的第一個(gè)步驟與字段解析一樣簿废,也需要先解析出類(lèi)方法表的class_index[3]項(xiàng)中索引的方法所屬的類(lèi)或接口的符號(hào)引用空入,如果解析成功,我們依然用C表示這個(gè)類(lèi)族檬,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的類(lèi)方法搜索歪赢。
1)類(lèi)方法和接口方法符號(hào)引用的常量類(lèi)型定義是分開(kāi)的,如果在類(lèi)方法表中發(fā)現(xiàn)class_index中索引的C是個(gè)接口单料,那就直接拋出java.lang.IncompatibleClassChangeError異常埋凯。
2)如果通過(guò)了第1步,在類(lèi)C中查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法扫尖,如果有則返回這個(gè)方法的直接引用白对,查找結(jié)束。
3)否則换怖,在類(lèi)C的父類(lèi)中遞歸查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法甩恼,如果有則返回這個(gè)方法的直接引用,查找結(jié)束沉颂。
4)否則条摸,在類(lèi)C實(shí)現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法,如果存在匹配的方法兆览,說(shuō)明類(lèi)C是一個(gè)抽象類(lèi)屈溉,這時(shí)查找結(jié)束,拋出java.lang.AbstractMethodError異常抬探。
5)否則子巾,宣告方法查找失敗帆赢,拋出java.lang.NoSuchMethodError。
最后线梗,如果查找過(guò)程成功返回了直接引用椰于,將會(huì)對(duì)這個(gè)方法進(jìn)行權(quán)限驗(yàn)證,如果發(fā)現(xiàn)不具備對(duì)此方法的訪問(wèn)權(quán)限仪搔,將拋出java.lang.IllegalAccessError異常瘾婿。
接口方法解析
接口方法也需要先解析出接口方法表的class_index[4]項(xiàng)中索引的方法所屬的類(lèi)或接口的符號(hào)引用,如果解析成功烤咧,依然用C表示這個(gè)接口偏陪,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的接口方法搜索。
1)與類(lèi)方法解析不同煮嫌,如果在接口方法表中發(fā)現(xiàn)class_index中的索引C是個(gè)類(lèi)而不是接口笛谦,那就直接拋出java.lang.IncompatibleClassChangeError異常。
2)否則昌阿,在接口C中查找是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法饥脑,如果有則返回這個(gè)方法的直接引用,查找結(jié)束懦冰。
3)否則灶轰,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類(lèi)(查找范圍會(huì)包括Object類(lèi))為止刷钢,看是否有簡(jiǎn)單名稱(chēng)和描述符都與目標(biāo)相匹配的方法笋颤,如果有則返回這個(gè)方法的直接引用,查找結(jié)束内地。
4)否則椰弊,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常瓤鼻。
由于接口中的所有方法默認(rèn)都是public的秉版,所以不存在訪問(wèn)權(quán)限的問(wèn)題,因此接口方法的符號(hào)解析應(yīng)當(dāng)不會(huì)拋出java.lang.IllegalAccessError異常茬祷。
五清焕、初始化
類(lèi)初始化階段是類(lèi)加載過(guò)程的最后一步,前面的類(lèi)加載過(guò)程中祭犯,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類(lèi)加載器參與之外秸妥,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段沃粗,才真正開(kāi)始執(zhí)行類(lèi)中定義的Java程序代碼粥惧。
在準(zhǔn)備階段,變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始值最盅。初始化階段是執(zhí)行類(lèi)構(gòu)造器<clinit>()方法的過(guò)程
<clinit>()方法是由編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的突雪,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的起惕,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量咏删,在前面的靜態(tài)語(yǔ)句塊可以賦值惹想,但是不能訪問(wèn)。
<clinit>()方法與類(lèi)的構(gòu)造函數(shù)(或者說(shuō)實(shí)例構(gòu)造器<init>()方法)不同督函,它不需要顯式地調(diào)用父類(lèi)構(gòu)造器嘀粱,虛擬機(jī)會(huì)保證在子類(lèi)的<clinit>()方法執(zhí)行之前,父類(lèi)的<clinit>()方法已經(jīng)執(zhí)行完畢辰狡。因此在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>()方法的類(lèi)肯定是java.lang.Object锋叨。
由于父類(lèi)的<clinit>()方法先執(zhí)行,也就意味著父類(lèi)中定義的靜態(tài)語(yǔ)句塊要優(yōu)先于子類(lèi)的變量賦值操作宛篇。
<clinit>()方法對(duì)于類(lèi)或接口來(lái)說(shuō)并不是必需的悲柱,如果一個(gè)類(lèi)中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作些己,那么編譯器可以不為這個(gè)類(lèi)生成<clinit>()方法。
接口中不能使用靜態(tài)語(yǔ)句塊嘿般,但仍然有變量初始化的賦值操作段标,因此接口與類(lèi)一樣都會(huì)生成<clinit>()方法。但接口與類(lèi)不同的是炉奴,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法逼庞。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化瞻赶。另外赛糟,接口的實(shí)現(xiàn)類(lèi)在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法。
虛擬機(jī)會(huì)保證一個(gè)類(lèi)的<clinit>()方法在多線程環(huán)境中被正確地加鎖砸逊、同步璧南,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的<clinit>()方法师逸,其他線程都需要阻塞等待司倚,直到活動(dòng)線程執(zhí)行<clinit>()方法完畢。如果在一個(gè)類(lèi)的<clinit>()方法中有耗時(shí)很長(zhǎng)的操作篓像,就可能造成多個(gè)進(jìn)程阻塞动知,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。