Java類(lèi)的初始化時(shí)機(jī)

一個(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)備之后):

  1. 遇到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í)候
  2. 使用java.lang.reflect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。

  3. 當(dāng)初始化一個(gè)類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化。

  4. 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。

  5. 當(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)景

  1. 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化
  2. 通過(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)的初始化
  3. 常量在編譯階段會(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 版本。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,564評(píng)論 19 139
  • C/C++在運(yùn)行前需要完成預(yù)處理、編譯、匯編、鏈接;而在Java中,類(lèi)加載(加載、連接、初始化)是在程序運(yùn)行期間第...
    Steven1997閱讀 1,034評(píng)論 1 2
  • 父類(lèi) static 塊 1 執(zhí)行 父類(lèi) 靜態(tài)成員staticSam1初始化 父類(lèi) 靜態(tài)成員staticSam2初始...
    YCix閱讀 1,402評(píng)論 0 0
  • 最近看的兩期都是與愛(ài)情相關(guān)的辯論。辯論,源于矛盾。這一期的辯題是在遭遇災(zāi)難的時(shí)候,伴侶手刀逃跑,該不該原諒。 節(jié)目...
    花生龜閱讀 1,594評(píng)論 0 3
  • 落日一抹黃 海天秋日長(zhǎng) 待裊裊余音繞梁 誰(shuí)還共我唱 舊年宮闕在 倒映虹橋殤 等彎彎河水流走 你可在梳妝 古風(fēng) 古韻...
    阿北偏東閱讀 255評(píng)論 0 2

友情鏈接更多精彩內(nèi)容