1. 程式人生 > >JVM 之 (13) 類載入機制——案例分析

JVM 之 (13) 類載入機制——案例分析

  在《JVM 之 (12) 類載入機制》一文中詳細闡述了類載入的過程,並舉了幾個例子進行了簡要分析,在文章的最後留了一個懸念給各位,這裡來揭開這個懸念。建議先看完《JVM 之 (12) 類載入機制》這篇再來看這個,印象會比較深刻,如若不然,也沒什麼關係~~ 
下面是程式程式碼:

package jvm.classload;

public class StaticTest
{
    public static void main(String[] args)
    {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static
    {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest()
    {
        System.out.println("3");
        System.out.println("a="+a+",b="+b);
    }

    public static void staticFunction(){
        System.out.println("4");
    }

    int a=110;
    static int b =112;
}

  問題是:請問這段程式的輸出是什麼? 
  這個是我在論壇上看到的一個問題,我覺得比較經典。 
  一般對於這類問題,小夥伴們腦海中肯定浮現出這樣的knowledge:

Java中賦值順序: 
1. 父類的靜態變數賦值 
2. 自身的靜態變數賦值 
3. 父類成員變數賦值和父類塊賦值 
4. 父類建構函式賦值 
5. 自身成員變數賦值和自身塊賦值 
6. 自身建構函式賦值

  ok,按照這個理論輸出是什麼呢?答案輸出:1 4,這樣正確嚒?肯定不正確啦,這裡不是說上面的規則不正確,而是說不能簡單的套用這個規則。 
  正確的答案是:

2
3
a=110,b=0
1
4

  是不是有點不可思議?且聽我一一道來,這裡主要的點之一:例項初始化不一定要在類初始化結束之後才開始初始化。 
  類的生命週期是:載入->驗證->準備->解析->初始化->使用->解除安裝,只有在準備階段和初始化階段才會涉及類變數的初始化和賦值,因此只針對這兩個階段進行分析; 
  類的準備階段需要做是為類變數分配記憶體並設定預設值,因此類變數st為null、b為0;(需要注意的是如果類變數是final,編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機器就會根據ConstantValue的設定將變數設定為指定的值,如果這裡這麼定義:static final

 int b=112,那麼在準備階段b的值就是112,而不再是0了。) 
  類的初始化階段需要做是執行類構造器(類構造器是編譯器收集所有靜態語句塊和類變數的賦值語句按語句在原始碼中的順序合併生成類構造器,物件的構造方法是<init>(),類的構造方法是<clinit>(),可以在堆疊資訊中看到),因此先執行第一條靜態變數的賦值語句即st = new StaticTest (),此時會進行物件的初始化,物件的初始化是先初始化成員變數再執行構造方法,因此設定a為110->列印2->執行構造方法(列印3,此時a已經賦值為110,但是b只是設定了預設值0,並未完成賦值動作),等物件的初始化完成後繼續執行之前的類構造器的語句,接下來就不詳細說了,按照語句在原始碼中的順序執行即可。 
  這裡面還牽涉到一個冷知識,就是在巢狀初始化時有一個特別的邏輯。特別是內嵌的這個變數恰好是個靜態成員,而且是本類的例項。 
  這會導致一個有趣的現象:“例項初始化竟然出現在靜態初始化之前”。 
  其實並沒有提前,你要知道java記錄初始化與否的時機。 
  看一個簡化的程式碼,把關鍵問題解釋清楚:

public class Test {
    public static void main(String[] args) {
        func();
    }
    static Test st = new Test();
    static void func(){}
}

  根據上面的程式碼,有以下步驟:

  1. 首先在執行此段程式碼時,首先由main方法的呼叫觸發靜態初始化。
  2. 在初始化Test 類的靜態部分時,遇到st這個成員。
  3. 但湊巧這個變數引用的是本類的例項。
  4. 那麼問題來了,此時靜態初始化過程還沒完成就要初始化例項部分了。是這樣麼?
  5. 從人的角度是的。但從java的角度,一旦開始初始化靜態部分,無論是否完成,後續都不會再重新觸發靜態初始化流程了。
  6. 因此在例項化st變數時,實際上是把例項初始化嵌入到了靜態初始化流程中,並且在樓主的問題中,嵌入到了靜態初始化的起始位置。這就導致了例項初始化完全至於靜態初始化之前。這也是導致a有值b沒值的原因。
  7. 最後再考慮到文字順序,結果就顯而易見了。

  詳細看到這裡,心中大概有個結論了吧,如果對於類的載入機制比較模糊的話,可以參考開篇推薦的博文~ 有問題歡迎留言。