一個(gè)Java對(duì)象的創(chuàng)建過(guò)程往往包括 類(lèi)初始化 和 類(lèi)實(shí)例化 兩個(gè)階段。本文討論的是『類(lèi)初始化』的時(shí)機(jī),以及利用這一特點(diǎn)實(shí)現(xiàn)單例模式的方法。
概述
我們知道,一個(gè).java文件在編譯后會(huì)形成相應(yīng)的一個(gè)或多個(gè)Class文件(若一個(gè)類(lèi)中含有內(nèi)部類(lèi),則編譯后會(huì)產(chǎn)生多個(gè)Class文件),但這些Class文件中描述的各種信息,最終都需要加載到虛擬機(jī)中之后才能被運(yùn)行和使用。事實(shí)上,虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn),轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類(lèi)型的過(guò)程就是虛擬機(jī)的 類(lèi)加載機(jī)制。
類(lèi)加載的時(shí)機(jī)
Java類(lèi)從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:
- 加載(Loading)
- 驗(yàn)證(Verification)
- 準(zhǔn)備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
這七個(gè)階段。其中準(zhǔn)備、驗(yàn)證、解析3個(gè)部分統(tǒng)稱(chēng)為連接(Linking),如圖所示:

加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類(lèi)的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)。
那么現(xiàn)在來(lái)回答這個(gè)問(wèn)題:虛擬機(jī)什么時(shí)候才會(huì)加載Class文件并初始化類(lèi)呢?
什么情況下虛擬機(jī)需要開(kāi)始加載一個(gè)類(lèi)呢?虛擬機(jī)規(guī)范中并沒(méi)有對(duì)此進(jìn)行強(qiáng)制約束,這點(diǎn)可以交給虛擬機(jī)的具體實(shí)現(xiàn)來(lái)自由把握。
而類(lèi)初始化時(shí)機(jī)比較復(fù)雜,下面我們具體來(lái)說(shuō)。
類(lèi)初始化時(shí)機(jī)
在虛擬機(jī)規(guī)范中是有嚴(yán)格規(guī)定的,虛擬機(jī)規(guī)范指明 有且只有 五種情況必須立即對(duì)類(lèi)進(jìn)行初始化(而這一過(guò)程自然發(fā)生在加載、驗(yàn)證、準(zhǔn)備之后):
-
遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時(shí),如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先對(duì)其進(jìn)行初始化。生成這四條指令的最常見(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í),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。
當(dāng)使用jdk1.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)行初始化,則需要先出觸發(fā)其初始化
注意,對(duì)于這五種會(huì)觸發(fā)類(lèi)進(jìn)行初始化的場(chǎng)景,虛擬機(jī)規(guī)范中使用了一個(gè)很強(qiáng)烈的限定語(yǔ):“有且只有”,這五種場(chǎng)景中的行為稱(chēng)為對(duì)一個(gè)類(lèi)進(jìn)行 主動(dòng)引用。除此之外,所有引用類(lèi)的方式,都不會(huì)觸發(fā)初始化,稱(chēng)為 被動(dòng)引用。
主動(dòng)使用的簡(jiǎn)易版本說(shuō)明
類(lèi)的初始化時(shí)機(jī)就是在"在首次主動(dòng)使用時(shí)",那么,哪些情形下才符合首次主動(dòng)使用的要求呢?首次主動(dòng)使用的情形:
- 創(chuàng)建某個(gè)類(lèi)的新實(shí)例時(shí)--new、反射、克隆或反序列化;
- 調(diào)用某個(gè)類(lèi)的靜態(tài)方法時(shí);
- 使用某個(gè)類(lèi)或接口的靜態(tài)字段或?qū)υ撟侄钨x值時(shí)(final字段除外);
- 調(diào)用Java的某些反射方法時(shí)
- 初始化某個(gè)類(lèi)的子類(lèi)時(shí)
- 在虛擬機(jī)啟動(dòng)時(shí)某個(gè)含有main()方法的那個(gè)啟動(dòng)類(lèi)。
除了以上幾種情形以外,所有其它使用JAVA類(lèi)型的方式都是被動(dòng)使用的,他們不會(huì)導(dǎo)致類(lèi)的初始化。
被動(dòng)引用的幾種經(jīng)典場(chǎng)景
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化
- 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化:newarray指令觸發(fā)的只是數(shù)組類(lèi)型本身的初始化,而不會(huì)導(dǎo)致其相關(guān)類(lèi)型的初始化,比如,new String[]只會(huì)直接觸發(fā)String[]類(lèi)的初始化,也就是觸發(fā)對(duì)類(lèi)[Ljava.lang.String的初始化,而直接不會(huì)觸發(fā)String類(lèi)的初始化
- 常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi),因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化
實(shí)現(xiàn)單例模式
利用類(lèi)初始化的特點(diǎn),我們可以實(shí)現(xiàn)線(xiàn)程安全的單例模式(不使用synchronized)。
餓漢式 static final field
這種方法非常簡(jiǎn)單,因?yàn)閱卫膶?shí)例被聲明成 static 和 final 變量了,在第一次加載類(lèi)到內(nèi)存中時(shí)就會(huì)初始化,所以創(chuàng)建實(shí)例本身是線(xiàn)程安全的。
public class Singleton{
//類(lèi)加載時(shí)就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
這種寫(xiě)法的缺點(diǎn)是它不是一種懶加載模式(lazy initialization),單例會(huì)在加載類(lèi)后一開(kāi)始就被初始化,即使客戶(hù)端沒(méi)有調(diào)用 getInstance()方法。
餓漢式的創(chuàng)建方式在一些場(chǎng)景中將無(wú)法使用:譬如 Singleton 實(shí)例的創(chuàng)建是依賴(lài)參數(shù)或者配置文件的,在 getInstance() 之前必須調(diào)用某個(gè)方法設(shè)置參數(shù)給它,那樣這種單例寫(xiě)法就無(wú)法使用了。
靜態(tài)內(nèi)部類(lèi) static nested class
下面這種方法既是線(xiàn)程安全的,又是Lazy加載的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種寫(xiě)法仍然使用JVM本身機(jī)制保證了線(xiàn)程安全問(wèn)題;由于 SingletonHolder 是私有的,除了 getInstance() 之外沒(méi)有辦法訪(fǎng)問(wèn)它,因此它是懶漢式的;同時(shí)讀取實(shí)例的時(shí)候不會(huì)進(jìn)行同步,沒(méi)有性能缺陷;也不依賴(lài) JDK 版本。