隨筆 - 14, 文章 - 39, 評論 - 17, 引用 - 0
          數(shù)據(jù)加載中……

          解析Java類和對象的初始化過程【轉(zhuǎn)】

          ???????????????出處:blog 責(zé)任編輯: 方舟

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

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

          問題引入

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

          [清單一]

          package com.ccb.framework.enums;
          import java.util.Collections;
          import java.util.HashMap;
          import java.util.Map;
          public class CachingEnumResolver {
           //單態(tài)實例 一切問題皆由此行引起
           private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new CachingEnumResolver();
           /*MSGCODE->Category內(nèi)存索引*/
           private static Map CODE_MAP_CACHE;
           static {
            CODE_MAP_CACHE = new HashMap();
            //為了說明問題,我在這里初始化一條數(shù)據(jù)
            CODE_MAP_CACHE.put("0","北京市");
           }
           //private, for single instance
           private CachingEnumResolver() {
            //初始化加載數(shù)據(jù) 引起問題,該方法也要負(fù)點責(zé)任
            initEnums();
           }
           /** * 初始化所有的枚舉類型 */
           public static void initEnums() {
            // ~~~~~~~~~問題從這里開始暴露 ~~~~~~~~~~~//
            if (null == CODE_MAP_CACHE) {
             System.out.println("CODE_MAP_CACHE為空,問題在這里開始暴露.");
             CODE_MAP_CACHE = new HashMap();
            }
            CODE_MAP_CACHE.put("1", "北京市");
            CODE_MAP_CACHE.put("2", "云南省");
            //..... other code...
           }
           public Map getCache() {
            return Collections.unmodifiableMap(CODE_MAP_CACHE);
           }
           /** * 獲取單態(tài)實例 * * @return */
           public static CachingEnumResolver getInstance() {
            return SINGLE_ENUM_RESOLVER;
           }
           public static void main(String[] args) {
            System.out.println(CachingEnumResolver.getInstance().getCache());
           }
          }

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

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

          [清單二]

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

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

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

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

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

          類初始化

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

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

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

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

           對象初始化

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

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

          通常來說,<init>() 方法內(nèi)包括的代碼內(nèi)容大概為:調(diào)用另一個 <init>() 方法;對實例變量初始化;與其對應(yīng)的構(gòu)造方法內(nèi)的代碼。 如果構(gòu)造方法是明確地從調(diào)用同一個類中的另一個構(gòu)造方法開始,那它對應(yīng)的 <init>() 方法體內(nèi)包括的內(nèi)容為:一個對本類的 <init>() 方法的調(diào)用;對應(yīng)用構(gòu)造方法內(nèi)的所有字節(jié)碼。

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

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

          類的初始化時機(jī)

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

          Java 虛擬機(jī)規(guī)范為類的初始化時機(jī)做了嚴(yán)格定義:"initialize on first active use"--" 在首次主動使用時初始化"。這個規(guī)則直接影響著類裝載、連接和初始化類的機(jī)制--因為在類型被初始化之前它必須已經(jīng)被連接,然而在連接之前又必須保證它已經(jīng)被裝載了。

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

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

          首次主動使用的情形:

          ·創(chuàng)建某個類的新實例時--new、反射、克隆或反序列化;

          ·調(diào)用某個類的靜態(tài)方法時;

          ·使用某個類或接口的靜態(tài)字段或?qū)υ撟侄钨x值時(final字段除外);

          ·調(diào)用Java的某些反射方法時

          ·初始化某個類的子類時

          ·在虛擬機(jī)啟動時某個含有main()方法的那個啟動類。

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

          我的問題究竟出在哪里

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

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

          [清單三]

          public class com.ccb.framework.enums.CachingEnumResolver extendsjava.lang.Object{
           static {};
           Code: 0: new #2;
           //class CachingEnumResolver
           3: dup
           4: invokespecial #14;
           //Method "<init>":()V ①
           7: putstatic #16;
           //Field SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;
           10: new #18;
           //class HashMap ②
           13: dup
           14: invokespecial #19;
           //Method java/util/HashMap."<init>":()V
           17: putstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           20: getstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           23: ldc #23;
           //String 0
           25: ldc #25;
           //String 北京市
           27: invokeinterface #31, 3;
           //InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; ③
           32: pop 33: returnprivate com.ccb.framework.enums.CachingEnumResolver();
           Code: 0: aload_0 1: invokespecial #34;
           //Method java/lang/Object."<init>":()V 4: invokestatic #37;
           //Method initEnums:()V ④ 7: returnpublic static void initEnums();
           Code: 0: getstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           ⑤ 3: ifnonnull 24 6: getstatic #44;
           //Field java/lang/System.out:Ljava/io/PrintStream;
           9: ldc #46;
           //String CODE_MAP_CACHE為空,問題在這里開始暴露.
           11: invokevirtual #52;
           //Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: new #18;
           //class HashMap 17: dup 18: invokespecial #19;
           //Method java/util/HashMap."<init>":()V ⑥ 21: putstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           24: getstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           27: ldc #54;
           //String 1 29: ldc #25;
           //String 北京市 31: invokeinterface #31, 3;
           //InterfaceMethod java/util/Map.put:(Ljava/lang/Object;
           Ljava/lang/Object;)Ljava/lang/Object;
          ?、?36: pop 37: getstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           40: ldc #56;
           //String 2 42: ldc #58;
           //String 云南省 44: invokeinterface #31, 3;
           //InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
           ⑧ 49: pop 50: returnpublic java.util.Map getCache();
           Code: 0: getstatic #21;
           //Field CODE_MAP_CACHE:Ljava/util/Map;
           3: invokestatic #66;
           //Method java/util/Collections.unmodifiableMap:(Ljava/util/Map;)Ljava/util/Map;
           6: areturnpublic static com.ccb.framework.enums.CachingEnumResolver getInstance();
           Code: 0: getstatic #16;
           //Field SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;
          ?、?3: areturn}

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

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

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

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

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

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

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

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

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

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

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

          通過對上面的字節(jié)碼執(zhí)行過程分析,或許你已經(jīng)清楚了解到導(dǎo)致錯誤的深層原因了,也或許你可能早已被上面的分析過程給弄得暈頭轉(zhuǎn)向了,不過也沒折,雖然我也可以從源代碼的角度來闡述問題,但這樣不夠深度,同時也會有僅為個人觀點、不足可信之嫌。

          如何解決

          要解決上面代碼所存在的問題很簡單,那就是將 "SINGLE_ENUM_RESOLVER" 變量的初始化賦值語句轉(zhuǎn)移到 getInstance() 方法中去即可。換句話說就是要避免在類還未初始化完成時從內(nèi)部實例化該類或在初始化過程中引用還未初始化的字段。

          寫在最后

          靜下浮燥之心,仔細(xì)思量自己是否真的掌握了本文主題所引出的知識,如果您覺得您已經(jīng)完全或基本掌握了,那么很好,在最后,我將前面的代碼稍做下修改,請思考下面兩組程序是否同樣會存在問題呢?

          程序一

          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();
           }

          最后,一點關(guān)于 JAVA 群體的感言:時下正是各種開源框架盛行時期,Spring 更是大行其道,吸引著一大批 JEE 開發(fā)者的眼球(我也是 fans 中的一員)。然而,讓我們仔細(xì)觀察一下--以 Spring 群體為例,在那么多的 Spring fans 當(dāng)中,有多少人去研究過 Spring 源代碼?又有多少人對 Spring 設(shè)計思想有真正深入了解呢?當(dāng)然,我是沒有資格以這樣的口吻來說事的,我只是想表明一個觀點--學(xué)東西一定要"正本清源"。



          posted on 2006-12-03 10:49 mlw2000 閱讀(237) 評論(0)  編輯  收藏 所屬分類: java

          主站蜘蛛池模板: 延边| 都兰县| 温州市| 民丰县| 绥滨县| 喀喇| 泰和县| 乐业县| 云阳县| 韶关市| 个旧市| 普定县| 东源县| 丽水市| 德惠市| 子长县| 天长市| 大城县| 焦作市| 鸡泽县| 仙居县| 万源市| 屏东县| 岱山县| 萨嘎县| 天气| 南汇区| 资兴市| 武胜县| 同仁县| 都兰县| 天气| 五峰| 全椒县| 松阳县| 北辰区| 循化| 保康县| 蓝田县| 漳州市| 德化县|