類加載機(jī)制
虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換解析,初始化,最終形成能被虛擬機(jī)直接使用的 Java 類型,這就是虛擬機(jī)的類加載機(jī)制。
類加載的時(shí)機(jī)
類從被加載到虛擬機(jī)開(kāi)始,到卸載出內(nèi)存為止,整個(gè)生命周期包括:
加載,驗(yàn)證,準(zhǔn)備,解析,初始化,使用,卸載
其中加載,驗(yàn)證,準(zhǔn)備,初始化,卸載這 5 個(gè)階段順序是確定的,類加載過(guò)程必須按照這種順序按部就班的開(kāi)始,而解析階段則不一定,解析階段在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持java語(yǔ)言的運(yùn)行時(shí)綁定(動(dòng)態(tài)綁定/晚期綁定)。
1 加載
加載階段主要完成 3 件事情:
- 通過(guò)類的全限定名來(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ì)象,作為這個(gè)類的各種數(shù)據(jù)在方法區(qū)的訪問(wèn)入口。
2 驗(yàn)證
目的:確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)損害虛擬機(jī)自身的安全。
- 文件格式校驗(yàn):字節(jié)流是否符合 Class 文件格式的規(guī)范,并能被當(dāng)前版本的虛擬機(jī)處理。
- 元數(shù)據(jù)校驗(yàn):字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,保證其描述的信息符合 Java 語(yǔ)言規(guī)范的要求。
- 字節(jié)碼校驗(yàn):通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的,符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)的安全事件。
- 符號(hào)引用校驗(yàn):這是最后一個(gè)階段的驗(yàn)證,它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生該轉(zhuǎn)化,后面會(huì)有講解),主要是對(duì)類自身以外的信息(常量池中的各種符號(hào)引用)進(jìn)行匹配性的校驗(yàn)。
3 準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。
對(duì)于該階段有以下幾點(diǎn)需要注意:
這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在Java堆中。
-
這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設(shè)一個(gè)類變量的定義為:
public static int value = 3;
那么變量 value 在準(zhǔn)備階段過(guò)后的初始值為 0,而不是 3,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何 Java 方法,而把 value 賦值為 3 的 putstatic 指令是在程序編譯后,存放于類構(gòu)造器 <clinit>() 方法之中的,所以把value 賦值為 3 的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
這里還需要注意如下幾點(diǎn):
- 對(duì)基本數(shù)據(jù)類型來(lái)說(shuō),對(duì)于類變量(static)和全局變量,如果不顯式地對(duì)其賦值而直接使用,則系統(tǒng)會(huì)為其賦予默認(rèn)的零值,而對(duì)于局部變量來(lái)說(shuō),在使用前必須顯式地為其賦值,否則編譯時(shí)不通過(guò)。
- 對(duì)于同時(shí)被 static 和 final 修飾的常量,必須在聲明的時(shí)候就為其顯式地賦值,否則編譯時(shí)不通過(guò);而只被 final 修飾的常量則既可以在聲明時(shí)顯式地為其賦值,也可以在類初始化時(shí)顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統(tǒng)不會(huì)為其賦予默認(rèn)零值。
- 對(duì)于引用數(shù)據(jù)類型 reference 來(lái)說(shuō),如數(shù)組引用、對(duì)象引用等,如果沒(méi)有對(duì)其進(jìn)行顯式地賦值而直接使用,系統(tǒng)都會(huì)為其賦予默認(rèn)的零值,即 null。
- 如果在數(shù)組初始化時(shí)沒(méi)有對(duì)數(shù)組中的各元素賦值,那么其中的元素將根據(jù)對(duì)應(yīng)的數(shù)據(jù)類型而被賦予默認(rèn)的零值。
數(shù)據(jù)類型 默認(rèn)零值 數(shù)據(jù)類型 默認(rèn)零值 int 0 boolean false long 0L float 0.0f short (short) 0 double 0.0d char '\u000' reference null byte (byte) 0 如果類字段的字段屬性表中存在 ConstantValue 屬性,即同時(shí)被 fina l和 static 修飾,那么在準(zhǔn)備階段變量 value 就會(huì)被初始化為 ConstValue 屬性所指定的值。
假設(shè)上面的類變量value被定義為: public static final int value = 3;
編譯時(shí) Javac 將會(huì)為 valu e生成 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù) ConstantValue 的設(shè)置將 value 賦值為3。我們可以理解為 static final 常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中。
4 解析
解析階段是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過(guò)程。
- 符號(hào)引用:符號(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)存中。
- 直接引用:直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那說(shuō)明引用的目標(biāo)必定已經(jīng)存在于內(nèi)存之中了。
前面說(shuō)解析階段可能開(kāi)始于初始化之前,也可能在初始化之后開(kāi)始,虛擬機(jī)會(huì)根據(jù)需要來(lái)判斷,到底是在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析(初始化之前),還是等到一個(gè)符號(hào)引用將要被使用前才去解析它(初始化之后)。
對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求時(shí)很常見(jiàn)的事情,虛擬機(jī)實(shí)現(xiàn)可能會(huì)對(duì)第一次解析的結(jié)果進(jìn)行緩存(在運(yùn)行時(shí)常量池中記錄直接引用,并把常量標(biāo)示為已解析狀態(tài)),從而避免解析動(dòng)作重復(fù)進(jìn)行。
解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法四類符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。
類或接口的解析:判斷所要轉(zhuǎn)化成的直接引用是對(duì)數(shù)組類型,還是普通的對(duì)象類型的引用,從而進(jìn)行不同的解析。
-
字段解析:對(duì)字段進(jìn)行解析時(shí),會(huì)先在本類中查找是否包含有簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,如果有,則查找結(jié)束;如果沒(méi)有,則會(huì)按照繼承關(guān)系從上往下遞歸搜索該類所實(shí)現(xiàn)的各個(gè)接口和它們的父接口,還沒(méi)有,則按照繼承關(guān)系從上往下遞歸搜索其父類,直至查找結(jié)束,查找流程如下圖所示:
imgclass Super{ public static int m = 11; static{ System.out.println("執(zhí)行了super類靜態(tài)語(yǔ)句塊"); } } class Father extends Super{ public static int m = 33; static{ System.out.println("執(zhí)行了父類靜態(tài)語(yǔ)句塊"); } } class Child extends Father{ static{ System.out.println("執(zhí)行了子類靜態(tài)語(yǔ)句塊"); } } public class StaticTest{ public static void main(String[] args){ System.out.println(Child.m); } } 執(zhí)行了super類靜態(tài)語(yǔ)句塊 執(zhí)行了父類靜態(tài)語(yǔ)句塊 33 //如果注釋掉Father類中對(duì)m定義的那一行,則輸出結(jié)果如下: 執(zhí)行了super類靜態(tài)語(yǔ)句塊 11 static變量發(fā)生在靜態(tài)解析階段,也即是初始化之前,此時(shí)已經(jīng)將字段的符號(hào)引用轉(zhuǎn)化為了內(nèi)存引用,也便將它與對(duì)應(yīng)的類關(guān)聯(lián)在了一起,由于在子類中沒(méi)有查找到與m相匹配的字段,那么m便不會(huì)與子類關(guān)聯(lián)在一起,因此并不會(huì)觸發(fā)子類的初始化。 最后需要注意:理論上是按照上述順序進(jìn)行搜索解析,但在實(shí)際應(yīng)用中,虛擬機(jī)的編譯器實(shí)現(xiàn)可能要比上述規(guī)范要求的更嚴(yán)格一些。如果有一個(gè)同名字段同時(shí)出現(xiàn)在該類的接口和父類中,或同時(shí)在自己或父類的接口中出現(xiàn),編譯器可能會(huì)拒絕編譯。如果對(duì)上面的代碼做些修改,將Super改為接口,并將Child類繼承Father類且實(shí)現(xiàn)Super接口,那么在編譯時(shí)會(huì)報(bào)出如下錯(cuò)誤: StaticTest.java:24: 對(duì) m 的引用不明確,F(xiàn)ather 中的 變量 m 和 Super 中的 變量 m 都匹配 System.out.println(Child.m);
類方法解析:對(duì)類方法的解析與對(duì)字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對(duì)類方法的匹配搜索,是先搜索父類,再搜索接口。
接口方法解析:與類方法解析步驟類似,知識(shí)接口不會(huì)有父類,因此,只遞歸向上搜索父接口就行了。
5 初始化
初始化是類加載過(guò)程的最后一步,到了此階段,才真正開(kāi)始執(zhí)行類中定義的Java程序代碼。在準(zhǔn)備階段,類變量已經(jīng)被賦過(guò)一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過(guò)程序指定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另一個(gè)角度來(lái)表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過(guò)程。
這里簡(jiǎn)單說(shuō)明下 <clinit>() 方法的執(zhí)行規(guī)則:
<clinit>() 方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句中可以賦值,但是不能訪問(wèn)。
<clinit>() 方法與實(shí)例構(gòu)造器<init>() 方法(類的構(gòu)造函數(shù))不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的 <clinit>() 方法執(zhí)行之前,父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢。因此,在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>() 方法的類肯定是 java.lang.Object。
<clinit>() 方法對(duì)于類或接口來(lái)說(shuō)并不是必須的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)類變量的賦值操作,那么編譯器可以不為這個(gè)類生成 <clinit>() 方法。
接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會(huì)生成 <clinit>() 方法。但是接口魚(yú)類不同的是:執(zhí)行接口的 <clinit>() 方法不需要先執(zhí)行父接口的 <clinit>() 方法,只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的 <clinit>() 方法。
虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖和同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 <clinit>() 方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,那就可能造成多個(gè)線程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。
下面給出一個(gè)簡(jiǎn)單的例子,以便更清晰地說(shuō)明如上規(guī)則:
class Father{
public static int a = 1;
static{
a = 2;
}
}
class Child extends Father{
public static int b = a;
}
public class ClinitTest{
public static void main(String[] args){
System.out.println(Child.b);
}
}
//執(zhí)行上面的代碼,會(huì)打印出2,也就是說(shuō)b的值被賦為了2。
首先在準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置類變量初始值,這樣 A 和 B 均被賦值為默認(rèn)值 0,而后再在調(diào)用<clinit>() 方法時(shí)給他們賦予程序中指定的值。當(dāng)我們調(diào)用 Child.b 時(shí),觸發(fā) Child 的 <clinit>() 方法,根據(jù)規(guī)則2,在此之前,要先執(zhí)行完其父類Father的 <clinit>() 方法,又根據(jù)規(guī)則 1,在執(zhí)行 <clinit>() 方法時(shí),需要按static語(yǔ)句或 static 變量賦值操作等在代碼中出現(xiàn)的順序來(lái)執(zhí)行相關(guān)的 static 語(yǔ)句,因此當(dāng)觸發(fā)執(zhí)行Father的<clinit>() 方法時(shí),會(huì)先將 a 賦值為 1,再執(zhí)行 static 語(yǔ)句塊中語(yǔ)句,將a賦值為2,而后再執(zhí)行Child類的<clinit>() 方法,這樣便會(huì)將b的賦值為 2.
//如果我們顛倒一下Father類中“public static int a = 1;”語(yǔ)句和“static語(yǔ)句塊”的順序,程序執(zhí)行后,則會(huì)打印出1。
很明顯是根據(jù)規(guī)則1,執(zhí)行Father的<clinit>()方法時(shí),根據(jù)順序先執(zhí)行了static語(yǔ)句塊中的內(nèi)容,后執(zhí)行了“public static int a = 1;”語(yǔ)句。
另外,在顛倒二者的順序之后,如果在static語(yǔ)句塊中對(duì)a進(jìn)行訪問(wèn)(比如將a賦給某個(gè)變量),在編譯時(shí)將會(huì)報(bào)錯(cuò),因?yàn)楦鶕?jù)規(guī)則1,它只能對(duì)a進(jìn)行賦值,而不能訪問(wèn)。