<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    ゞ沉默是金ゞ

    魚離不開水,但是沒有說不離開哪滴水.
    posts - 98,comments - 104,trackbacks - 0

    類的初始化和對(duì)象初始化是 JVM 管理的類型生命周期中非常重要的兩個(gè)環(huán)節(jié),Google 了一遍網(wǎng)絡(luò),有關(guān)類裝載機(jī)制的文章倒是不少,然而類初始化和對(duì)象初始化的文章并不多,特別是從字節(jié)碼和 JVM 層次來分析的文章更是鮮有所見。

    本文主要對(duì)類和對(duì)象初始化全過程進(jìn)行分析,通過一個(gè)實(shí)際問題引入,將源代碼轉(zhuǎn)換成 JVM 字節(jié)碼后,對(duì) JVM 執(zhí)行過程的關(guān)鍵點(diǎn)進(jìn)行全面解析,并在文中穿插入了相關(guān) JVM 規(guī)范和 JVM 的部分內(nèi)部理論知識(shí),以理論與實(shí)際結(jié)合的方式介紹對(duì)象初始化和類初始化之間的協(xié)作以及可能存在的沖突問題。

    問題引入

    近日我在調(diào)試一個(gè)枚舉類型的解析器程序,該解析器是將數(shù)據(jù)庫內(nèi)一萬多條枚舉代碼裝載到緩存中,為了實(shí)現(xiàn)快速定位枚舉代碼和具體枚舉類別的所有枚舉元素,該類在裝載枚舉代碼的同時(shí)對(duì)其采取兩種策略建立內(nèi)存索引。由于該類是一個(gè)公共服務(wù)類,在程序各個(gè)層面都會(huì)使用到它,因此我將它實(shí)現(xiàn)為一個(gè)單例類。這個(gè)類在我調(diào)整類實(shí)例化語句位置之前運(yùn)行正常,但當(dāng)我把該類實(shí)例化語句調(diào)整到靜態(tài)初始化語句之前時(shí),我的程序不再為我工作了。

    下面是經(jīng)過我簡(jiǎn)化后的示例代碼:

    package com.redoffice.spreadsheets.data;

    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;

    public class CachingEnumResolver {
        
    // 單態(tài)實(shí)例 一切問題皆由此行引起
        private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new CachingEnumResolver();
        
    /* MSGCODE->Category內(nèi)存索引 */
        
    private static Map<String, String> CODE_MAP_CACHE;
        
    static {
            CODE_MAP_CACHE 
    = new HashMap<String, String>();
            
    // 為了說明問題,我在這里初始化一條數(shù)據(jù)
            CODE_MAP_CACHE.put("0""北京市");
        }


        
    // private, for single instance
        private CachingEnumResolver() {
            
    // 初始化加載數(shù)據(jù) 引起問題,該方法也要負(fù)點(diǎn)責(zé)任
            initEnums();
        }


        
    /**
         * 初始化所有的枚舉類型
         
    */

        
    public static void initEnums() {
            
    // ~~~~~~~~~問題從這里開始暴露 ~~~~~~~~~~~//
            if (null == CODE_MAP_CACHE) {
                System.out.println(
    "CODE_MAP_CACHE為空,問題在這里開始暴露.");
                CODE_MAP_CACHE 
    = new HashMap<String, String>();
            }

            CODE_MAP_CACHE.put(
    "1""北京市");
            CODE_MAP_CACHE.put(
    "2""云南省");

            
    // .. other code
        }


        
    public Map<String, String> getCache() {
            
    return Collections.unmodifiableMap(CODE_MAP_CACHE);
        }


        
    /**
         * 獲取單態(tài)實(shí)例
         * 
         * 
    @return
         
    */

        
    public static CachingEnumResolver getInstance() {
            
    return SINGLE_ENUM_RESOLVER;
        }


        
    public static void main(String[] args) {
            System.out.println(CachingEnumResolver.getInstance().getCache());
        }


    }


    想必大家看了上面的代碼后會(huì)感覺有些茫然,這個(gè)類看起來沒有問題啊,這的確屬于典型的餓漢式單態(tài)模式啊,怎么會(huì)有問題呢?

    是的,他看起來的確沒有問題,可是如果將他 run 起來時(shí),其結(jié)果是他不會(huì)為你正確 work。運(yùn)行該類,它的執(zhí)行結(jié)果是:


    CODE_MAP_CACHE為空,問題在這里開始暴露.
    {0=北京市}

    我的程序怎么會(huì)這樣?為什么在 initEnum() 方法里 CODE_MAP_CACHE 為空?為什么我輸出的 CODE_MAP_CACHE 內(nèi)容只有一個(gè)元素,其它兩個(gè)元素呢????!!

    看到這里,如果是你在調(diào)試該程序,你此刻一定覺得很奇怪,難道是我的 Jvm 有問題嗎?非也!如果不是,那我的程序是怎么了?這絕對(duì)不是我想要的結(jié)果。可事實(shí)上無論怎么修改 initEnum() 方法都無濟(jì)于事,起碼我最初是一定不會(huì)懷疑到問題可能出在創(chuàng)建 CachingEnumResolver 實(shí)例這一環(huán)節(jié)上。正是因?yàn)槲姨嘈盼覄?chuàng)建 CachingEnumResolver 實(shí)例的方法,加之對(duì) Java 類初始化與對(duì)象實(shí)例化底層原理理解有所偏差,使我為此付出了三、四個(gè)小時(shí)--約半個(gè)工作日的大好青春。

    那么問題究竟出在哪里呢?為什么會(huì)出現(xiàn)這樣的怪事呢?在解決這個(gè)問題之前,先讓我們來了解一下JVM的類和對(duì)象初始化的底層機(jī)制。

    類的生命周期

    上圖展示的是類生命周期流向;在本文里,我只打算談?wù)勵(lì)惖?初始化"以及"對(duì)象實(shí)例化"兩個(gè)階段。

    類初始化

    類"初始化"階段,它是一個(gè)類或接口被首次使用的前階段中的最后一項(xiàng)工作,本階段負(fù)責(zé)為類變量賦予正確的初始值。

    Java 編譯器把所有的類變量初始化語句和類型的靜態(tài)初始化器通通收集到 <clinit> 方法內(nèi),該方法只能被 Jvm 調(diào)用,專門承擔(dān)初始化工作。

    除接口以外,初始化一個(gè)類之前必須保證其直接超類已被初始化,并且該初始化過程是由 Jvm 保證線程安全的。另外,并非所有的類都會(huì)擁有一個(gè) <clinit>() 方法,在以下條件中該類不會(huì)擁有 <clinit>() 方法:

    • 該類既沒有聲明任何類變量,也沒有靜態(tài)初始化語句;
    • 該類聲明了類變量,但沒有明確使用類變量初始化語句或靜態(tài)初始化語句初始化;
    • 該類僅包含靜態(tài) final 變量的類變量初始化語句,并且類變量初始化語句是編譯時(shí)常量表達(dá)式。

    對(duì)象初始化

    在類被裝載、連接和初始化,這個(gè)類就隨時(shí)都可能使用了。對(duì)象實(shí)例化和初始化是就是對(duì)象生命的起始階段的活動(dòng),在這里我們主要討論對(duì)象的初始化工作的相關(guān)特點(diǎn)。

    Java 編譯器在編譯每個(gè)類時(shí)都會(huì)為該類至少生成一個(gè)實(shí)例初始化方法--即 "<init>()" 方法。此方法與源代碼中的每個(gè)構(gòu)造方法相對(duì)應(yīng),如果類沒有明確地聲明任何構(gòu)造方法,編譯器則為該類生成一個(gè)默認(rèn)的無參構(gòu)造方法,這個(gè)默認(rèn)的構(gòu)造器僅僅調(diào)用父類的無參構(gòu)造器,與此同時(shí)也會(huì)生成一個(gè)與默認(rèn)構(gòu)造方法對(duì)應(yīng)的 "<init>()" 方法.

    通常來說,<init>() 方法內(nèi)包括的代碼內(nèi)容大概為:調(diào)用另一個(gè) <init>() 方法;對(duì)實(shí)例變量初始化;與其對(duì)應(yīng)的構(gòu)造方法內(nèi)的代碼。

    如果構(gòu)造方法是明確地從調(diào)用同一個(gè)類中的另一個(gè)構(gòu)造方法開始,那它對(duì)應(yīng)的 <init>() 方法體內(nèi)包括的內(nèi)容為:一個(gè)對(duì)本類的 <init>() 方法的調(diào)用;對(duì)應(yīng)用構(gòu)造方法內(nèi)的所有字節(jié)碼。

    如果構(gòu)造方法不是通過調(diào)用自身類的其它構(gòu)造方法開始,并且該對(duì)象不是 Object 對(duì)象,那 <init>() 法內(nèi)則包括的內(nèi)容為:一個(gè)對(duì)父類 <init>() 方法的調(diào)用;對(duì)實(shí)例變量初始化方法的字節(jié)碼;最后是對(duì)應(yīng)構(gòu)造子的方法體字節(jié)碼。

    如果這個(gè)類是 Object,那么它的 <init>() 方法則不包括對(duì)父類 <init>() 方法的調(diào)用。

    類的初始化時(shí)機(jī)

    本文到目前為止,我們已經(jīng)大概有了解到了類生命周期中都經(jīng)歷了哪些階段,但這個(gè)類的生命周期的開始階段--類裝載又是在什么時(shí)候被觸發(fā)呢?類又是何時(shí)被初始化的呢?讓我們帶著這三個(gè)疑問繼續(xù)去尋找答案。

    Java 虛擬機(jī)規(guī)范為類的初始化時(shí)機(jī)做了嚴(yán)格定義:"initialize on first active use"--" 在首次主動(dòng)使用時(shí)初始化"。這個(gè)規(guī)則直接影響著類裝載、連接和初始化類的機(jī)制--因?yàn)樵陬愋捅怀跏蓟八仨氁呀?jīng)被連接,然而在連接之前又必須保證它已經(jīng)被裝載了。

    在與初始化時(shí)機(jī)相關(guān)的類裝載時(shí)機(jī)問題上,Java 虛擬機(jī)規(guī)范并沒有對(duì)其做嚴(yán)格的定義,這就使得 JVM 在實(shí)現(xiàn)上可以根據(jù)自己的特點(diǎn)提供采用不同的裝載策略。我們可以思考一下 Jboss AOP 框架的實(shí)現(xiàn)原理,它就是在對(duì)你的 class 文件裝載環(huán)節(jié)做了手腳--插入了 AOP 的相關(guān)攔截字節(jié)碼,這使得它可以對(duì)程序員做到完全透明化,哪怕你用 new 操作符創(chuàng)建出的對(duì)象實(shí)例也一樣能被 AOP 框架攔截--與之相對(duì)應(yīng)的 Spring AOP,你必須通過他的 BeanFactory 獲得被 AOP 代理過的受管對(duì)象,當(dāng)然 Jboss AOP 的缺點(diǎn)也很明顯--他是和 JBOSS 服務(wù)器綁定很緊密的,你不能很輕松的移植到其它服務(wù)器上。嗯~……,說到這里有些跑題了,要知道 AOP 實(shí)現(xiàn)策略足可以寫一本厚厚的書了,嘿嘿,就此打住。

    說了這么多,類的初始化時(shí)機(jī)就是在"在首次主動(dòng)使用時(shí)",那么,哪些情形下才符合首次主動(dòng)使用的要求呢?

    首次主動(dòng)使用的情形:

    • 創(chuàng)建某個(gè)類的新實(shí)例時(shí)--new、反射、克隆或反序列化;
    • 調(diào)用某個(gè)類的靜態(tài)方法時(shí);
    • 使用某個(gè)類或接口的靜態(tài)字段或?qū)υ撟侄钨x值時(shí)(final字段除外);
    • 調(diào)用Java的某些反射方法時(shí)
    • 初始化某個(gè)類的子類時(shí)
    • 在虛擬機(jī)啟動(dòng)時(shí)某個(gè)含有main()方法的那個(gè)啟動(dòng)類。

    除了以上幾種情形以外,所有其它使用JAVA類型的方式都是被動(dòng)使用的,他們不會(huì)導(dǎo)致類的初始化。

    我的問題究竟出在哪里

    好了,了解了JVM的類初始化與對(duì)象初始化機(jī)制后,我們就有了理論基礎(chǔ),也就可以理性的去分析問題了。

    下面讓我們來看看前面[清單一]的JAVA源代碼反組譯出的字節(jié)碼:

    Compiled from "CachingEnumResolver.java"
    public class com.redoffice.spreadsheets.data.CachingEnumResolver extends java.la
    ng.Object
    {
    public static void initEnums();
      Code:
       
    0:   aconst_null
       
    1:   getstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       4:   if_acmpne       25
       
    7:   getstatic       #4//Field java/lang/System.out:Ljava/io/PrintStream;
       10:  ldc     #5//String CODE_MAP_CACHE涓虹┖,闂鍦ㄨ繖閲屽紑濮嬫毚闇?.
       12:  invokevirtual   #6//Method java/io/PrintStream.println:(Ljava/lang/Str
    ing;)V
       
    15:  new     #7//class java/util/HashMap
       18:  dup
       
    19:  invokespecial   #8//Method java/util/HashMap."<init>":()V
       22:  putstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       25:  getstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       28:  ldc     #9//String 1
       30:  ldc     #10//String 鍖椾含甯?
       32:  invokeinterface #11,  3//InterfaceMethod java/util/Map.put:(Ljava/lang
    /Object;Ljava/lang/Object;)Ljava/lang/Object;
       
    37:  pop
       
    38:  getstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       41:  ldc     #12//String 2
       43:  ldc     #13//String 浜戝崡鐪?
       45:  invokeinterface #11,  3//InterfaceMethod java/util/Map.put:(Ljava/lang
    /Object;Ljava/lang/Object;)Ljava/lang/Object;
       
    50:  pop
       
    51:  return

    public java.util.Map getCache();
      Code:
       
    0:   getstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       3:   invokestatic    #14//Method java/util/Collections.unmodifiableMap:(Lja
    va/util/Map;)Ljava/util/Map;
       
    6:   areturn

    public static com.redoffice.spreadsheets.data.CachingEnumResolver getInstance();

      Code:
       
    0:   getstatic       #15//Field SINGLE_ENUM_RESOLVER:Lcom/redoffice/spreads
    heets/data/CachingEnumResolver;
       
    3:   areturn

    public static void main(java.lang.String[]);
      Code:
       
    0:   getstatic       #4//Field java/lang/System.out:Ljava/io/PrintStream;
       3:   invokestatic    #16//Method getInstance:()Lcom/redoffice/spreadsheets/
    data/CachingEnumResolver;
       
    6:   invokevirtual   #17//Method getCache:()Ljava/util/Map;
       9:   invokevirtual   #18//Method java/io/PrintStream.println:(Ljava/lang/Ob
    ject;)V
       
    12:  return

    static {};
      Code:
       
    0:   new     #19//class com/redoffice/spreadsheets/data/CachingEnumResolver

       
    3:   dup
       
    4:   invokespecial   #20//Method "<init>":()V
       7:   putstatic       #15//Field SINGLE_ENUM_RESOLVER:Lcom/redoffice/spreads
    heets/data/CachingEnumResolver;
       
    10:  new     #7//class java/util/HashMap
       13:  dup
       
    14:  invokespecial   #8//Method java/util/HashMap."<init>":()V
       17:  putstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       20:  getstatic       #3//Field CODE_MAP_CACHE:Ljava/util/Map;
       23:  ldc     #21//String 0
       25:  ldc     #10//String 鍖椾含甯?
       27:  invokeinterface #11,  3//InterfaceMethod java/util/Map.put:(Ljava/lang
    /Object;Ljava/lang/Object;)Ljava/lang/Object;
       
    32:  pop
       
    33:  return

    }

    如果上面顯示,清單內(nèi)容是在 JDK1.4 環(huán)境下的字節(jié)碼內(nèi)容,可能這份清單對(duì)于很大部分兄弟來說確實(shí)沒有多少吸引力,因?yàn)檫@些 JVM 指令確實(shí)不像源代碼那樣漂亮易懂。但它的的確確是查找和定位問題最直接的辦法,我們想要的答案就在這份 JVM 指令清單里。

    現(xiàn)在,讓我們對(duì)該類從類初始化到對(duì)象實(shí)例初始化全過程分析[清單一]中的代碼執(zhí)行軌跡。

    如前面所述,類初始化是在類真正可用時(shí)的最后一項(xiàng)前階工作,該階段負(fù)責(zé)對(duì)所有類正確的初始化值,此項(xiàng)工作是線程安全的,JVM會(huì)保證多線程同步。

    第1步:調(diào)用類初始化方法 CachingEnumResolver.<clinit>(),該方法對(duì)外界是不可見的,換句話說是 JVM 內(nèi)部專用方法,<clinit>() 內(nèi)包括了 CachingEnumResolver 內(nèi)所有的具有指定初始值的類變量的初始化語句。要注意的是并非每個(gè)類都具有該方法,具體的內(nèi)容在前面已有敘述。

    第2步:進(jìn)入 <clinit>() 方法內(nèi),讓我們看字節(jié)碼中的 "①" 行,該行與其上面兩行組合起來代表 new 一個(gè) CachingEnumResolver 對(duì)象實(shí)例,而該代碼行本身是指調(diào)用 CachingEnumResolver 類的 <init>()方法。每一個(gè) Java 類都具有一個(gè) <init>() 方法,該方法是 Java 編譯器在編譯時(shí)生成的,對(duì)外界不可見,<init>() 方法內(nèi)包括了所有具有指定初始化值的實(shí)例變量初始化語句和java類的構(gòu)造方法內(nèi)的所有語句。對(duì)象在實(shí)例化時(shí),均通過該方法進(jìn)行初始化。然而到此步,一個(gè)潛在的問題已經(jīng)在此埋伏好,就等著你來犯了。

    第3步:讓我們順著執(zhí)行順序向下看,"④" 行,該行所在方法就是該類的構(gòu)造器,該方法先調(diào)用父類的構(gòu)造器 <init>() 對(duì)父對(duì)象進(jìn)行初始化,然后調(diào)用 CachingEnumResolver.initEnum() 方法加載數(shù)據(jù)。

    第4步:"⑤" 行,該行獲取 "CODE_MAP_CACHE" 字段值,其運(yùn)行時(shí)該字段值為 null。注意,問題已經(jīng)開始顯現(xiàn)了。(作為程序員的你一定是希望該字段已經(jīng)被初始化過了,而事實(shí)上它還沒有被初始化)。通過判斷,由于該字段為 NULL,因此程序?qū)⒗^續(xù)執(zhí)行到 "⑥" 行,將該字段實(shí)例化為 HashMap()。

    第5步:在 "⑦"、"⑧" 行,其功能就是為 "CODE_MAP_CACHE" 字段填入兩條數(shù)據(jù)。

    第6步:退出對(duì)象初始化方法 <init>(),將生成的對(duì)象實(shí)例初始化給類字段 "SINGLE_ENUM_RESOLVER"。(注意,此刻該對(duì)象實(shí)例內(nèi)的類變量還未初始化完全,剛才由 <init>() 調(diào)用 initEnum() 方法賦值的類變量 "CODE_MAP_CACHE" 是 <clinit>() 方法還未初始化字段,它還將在后面的類初始化過程再次被覆蓋)。

    第7步:繼續(xù)執(zhí)行 <clinit>()方法內(nèi)的后繼代碼,"②" 行,該行對(duì) "CODE_MAP_CACHE" 字段實(shí)例化為 HashMap 實(shí)例(注意:在對(duì)象實(shí)例化時(shí)已經(jīng)對(duì)該字段賦值過了,現(xiàn)在又重新賦值為另一個(gè)實(shí)例,此刻,"CODE_MAP_CACHE"變量所引用的實(shí)例的類變量值被覆蓋,到此我們的疑問已經(jīng)有了答案)。

    第8步:類初始化完畢,同時(shí)該單態(tài)類的實(shí)例化工作也完成。

    通過對(duì)上面的字節(jié)碼執(zhí)行過程分析,或許你已經(jīng)清楚了解到導(dǎo)致錯(cuò)誤的深層原因了,也或許你可能早已被上面的分析過程給弄得暈頭轉(zhuǎn)向了,不過也沒折,雖然我也可以從源代碼的角度來闡述問題,但這樣不夠深度,同時(shí)也會(huì)有僅為個(gè)人觀點(diǎn)、不足可信之嫌。
    如何解決
    要解決上面代碼所存在的問題很簡(jiǎn)單,那就是將 "SINGLE_ENUM_RESOLVER" 變量的初始化賦值語句轉(zhuǎn)移到 getInstance() 方法中去即可。換句話說就是要避免在類還未初始化完成時(shí)從內(nèi)部實(shí)例化該類或在初始化過程中引用還未初始化的字段。
    寫在最后

        public class CachingEnumResolver {
        
    public  static Map CODE_MAP_CACHE;
        
    static {
            CODE_MAP_CACHE 
    = new HashMap();
            
    //為了說明問題,我在這里初始化一條數(shù)據(jù)
            CODE_MAP_CACHE.put("0","北京市");
            initEnums();
        }


        public class CachingEnumResolver {
            
    private static final CachingEnumResolver SINGLE_ENUM_RESOLVER;
            
    public  static Map CODE_MAP_CACHE;
            
    static {
                CODE_MAP_CACHE 
    = new HashMap();
                
    //為了說明問題,我在這里初始化一條數(shù)據(jù)
                CODE_MAP_CACHE.put("0","北京市");
                SINGLE_ENUM_RESOLVER 
    = new CachingEnumResolver();
                initEnums();
            }


    注:以上來源于網(wǎng)絡(luò)
    posted on 2009-03-31 15:17 ゞ沉默是金ゞ 閱讀(1387) 評(píng)論(1)  編輯  收藏 所屬分類: Java SE

    FeedBack:
    # re: 再談?lì)惖某跏蓟?a name="Post">
    2009-04-01 12:00 | sven
    換句話說就是要避免在類還未初始化完成時(shí)從內(nèi)部實(shí)例化該類或在初始化過程中引用還未初始化的字段。

    這句才是本文的主旨  回復(fù)  更多評(píng)論
      
    主站蜘蛛池模板: 日韩精品无码免费视频| 国产曰批免费视频播放免费s| 亚洲国产成人久久综合一| 大学生一级毛片免费看| 不卡视频免费在线观看| 亚洲一本到无码av中文字幕| 亚洲中文字幕无码一区二区三区| 美女视频黄a视频全免费| 99re视频精品全部免费| 51午夜精品免费视频| 亚洲av日韩综合一区二区三区| 亚洲欧洲校园自拍都市| 亚洲AV无码码潮喷在线观看| 亚洲人成无码久久电影网站| 日韩午夜免费视频| 1000部啪啪未满十八勿入免费| 日本免费久久久久久久网站| caoporn国产精品免费| 久久久久久夜精品精品免费啦| 九九全国免费视频| 春意影院午夜爽爽爽免费| 国产精品亚洲一区二区无码 | 亚洲乱码卡三乱码新区| 亚洲AV日韩AV永久无码久久 | 国产成人在线观看免费网站| 最近的免费中文字幕视频| 亚洲高清视频免费| 免费萌白酱国产一区二区| 国产美女无遮挡免费视频网站 | 亚洲欧洲国产精品香蕉网| 亚洲人成77777在线播放网站| 亚洲国产成+人+综合| eeuss免费天堂影院| 好男人www免费高清视频在线| 成人看的午夜免费毛片| 亚洲综合国产一区二区三区| 97久久国产亚洲精品超碰热| 亚洲综合色一区二区三区| 国产永久免费高清在线| 免费国产黄网站在线观看可以下载 | 久久国产精品萌白酱免费|