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

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

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:app

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

  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記錄初始化與否的時機。 
  看一個簡化的代碼,把關鍵問題解釋清楚:
spa

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

  根據上面的代碼,有如下步驟:.net

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

  詳細看到這裏,心中大概有個結論了吧,若是對於類的加載機制比較模糊的話,能夠參考開篇推薦的博文~ 有問題歡迎留言。


對象

相關文章
相關標籤/搜索