通過分析類加載過程來解析Java的靜態變量、靜態方法、靜態代碼塊、代碼塊、構造器執行順序

一、 前言

本篇文章主要是通過解析類加載過程來驗證子父類之間的靜態方法、靜態代碼塊、普通方法、代碼塊、構造器的執行順序。

二、 類加載過程

類加載指的是在程序運行期將類數據從Class文件加載到內存中,最終形成可以被虛擬機直接使用的Java類型,整個過程包括加載、連接(驗證、準備、解析)、初始化5個階段。

類加載的時機

在開始類加載過程講述之前,先聊一聊類加載是什麼時候開始的。虛擬機沒有規定什麼時候進行類的加載,但規定了在什麼情況下需要立即對類進行初始化。(當然在這之前需要對該類完成加載、驗證、準備、解析)

以下五種情況爲主動引用,需要加載類:
1.遇到new、getStatic、putstatic、invokeStatic這四條字節碼指令時,new(實例對象),getStatic(讀取一個類的靜態字段),putstatic(設置一個類的靜態字段),invokeStatic(調用一個類的靜態方法)。

2.使用Java.lang.reflect包的方法對類進行反射調用時,如果此時類沒有進行init,會先init。

3.當初始化一個類時,如果其父類沒有進行初始化,先初始化父類。

4.jvm啓動時,用戶需要指定一個執行的主類(包含main的類)虛擬機會先執行這個類。

5.當使用JDK1.7的動態語言支持的時候,當java.lang.invoke.MethodHandler實例後的結果是REF-getStatic/REF_putstatic/REF_invokeStatic的句柄,並且這些句柄對應的類沒初始化的話應該首先初始。

以下三種情況稱作被動引用,不需要加載類:
1、通過子類引用父類的靜態字段或者父類特有靜態方法,不會導致子類初始化(子類是否會進行加載、連接等過程取決於虛擬機的參數設置)。

2、通過數組定義來引用類,不會觸發此類的初始化。

3、調用類B使用類A的常量變量時,不會觸發常量所在的類A的初始化,A類的常量在編譯階段會存入調用類B的常量池中,類B看起來是訪問類A的常量,實際上是在自身的常量池(B類的常量池)中訪問該常量。

注意: 接口加載過程和類加載基本相同,雖然接口不能使用static方法塊,但還是會初始化接口成員變量(默認且只能爲public static final,不可修改)。接口初始化時不要求父接口都完成初始化,只要在用到時完成即可。

加載

加載過程完成以下三件事情:
1、使用類加載器來通過一個類的全限定名來獲取其定義的二進制字節流。

2、將這個字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據結構。

3、在堆中生成一個代表這個類的Class對象,作爲方法區中這些數據的訪問入口。

特別注意:
1、類加載器使用的是雙親委託機制,從下向上進行檢查、從上向下進行嘗試加載,保證了核心類不會被篡改,防止了類被重複加載。

2、通過類的全限定名進行加載,表明並不一定非要從Class文件中獲取二進制字節流,而可以通過多種其它途徑,例如ZIP包、網絡、其它文件、數據庫,由此產生了許多中Java技術。

3、數組類本身不是通過類加載器加載,而是由虛擬機直接創建,但數組類中的元素類型需要通過類加載器去加載,引用類型元素使用相應的類加載器進行加載,基本類型元素,將其標記爲與引導類加載器關聯。

4、加載開始時間和後面的連接階段開始時間是固定先後順序的,但兩個過程往往是交叉進行的,加載還未完成,連接階段已經開始。

5、加載階段用戶可通過自定義加載器就行類加載,連接階段完全由虛擬機主導和控制,初始階段纔開始真正的執行java的程序代碼(字節碼)。

驗證

驗證是連接的第一個階段,驗證的對象是字節流,主要是確保字節流中包含的信息符合當前虛擬機的要求,不會危害虛擬機自身安全。包括文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證四個階段。

1、文件格式的驗證:驗證Class文件字節流是否符合Class文件的格式的規範,並且能夠被當前版本的虛擬機處理,確保字節流能正確地解析並存儲於方法區中。這裏面主要對魔數、主次版本號、常量池等等的校驗。
(注意: 完成文件格式驗證之後,字節流進入了方法區,之後的三個驗證階段都是基於方法區的存儲結構進行的,不再直接操作字節流。)

2、數據驗證:對數據類型進行校驗。主要是對字節碼描述的信息進行語義分析,語義校驗元數據信息,以保證其描述的信息符合java語言規範的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類衝突、是否繼承了final類、重載不符合規則等等。

3、字節碼驗證:對方法體進行校驗。這是整個驗證過程最複雜的階段,主要是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在元數據驗證階段對數據類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運行時不會做出危害虛擬機安全的事。比如:保證方法體的類型轉換是否有效、保證操作數棧的數據類型與指令代碼能正常工作等。

4、符號引用驗證:它是驗證的最後一個階段,發生在虛擬機將符號引用轉化爲直接引用的時候。主要是對常量池中的各種符號引用進行匹配校驗。目的是確保解析動作能夠完成。比如:校驗通過字符串全限定名是否能找到該類、當前類是否有權限訪問符號引用中的類、字段、方法,校驗類中是否存在符號引用所描述的方法和字段。

注意: 可通過-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備

準備階段開始在方法區中爲類變量(靜態變量)分配內存,並設置變量初始值,需要注意的是這裏的初始值,通常情況下並不是自己在代碼中賦予的值,而是虛擬機默認的零值。如下:
在這裏插入圖片描述
但當類變量是一個常量時,就會被賦予代碼中的初始值。因爲當該類進行編譯時,編譯器會爲常量生成ConstantValue屬性,並按照代碼初始化的值進行指定ConstantValue屬性的值。當一個類字段的字段屬性中有ConstantValue屬性,在準備階段,就按照ConstantValue屬性的值爲該類變量賦值。

  public static int value=666;

以上代碼,在準備階段,value的值爲0。

  public static final int value=666;

以上代碼,在準備階段,value的值爲666。

解析

解析階段主要是虛擬機將常量池中的符號引用轉化爲直接引用。

符號引用:相當於一個邏輯引用。以一組符號來描述所引用的目標,可以是任何形式的字面量,只要是能無歧義的定位到目標就好。因爲符號引用的字面量形式明確定義在了Java虛擬機規範的Class文件格式中,因此各種虛擬機雖然內存佈局可以不同,但能接受的符號引用必須一致。符號引用同虛擬機實現的內存佈局無關,符號引用的目標不一定加載到了內存中。

直接引用:相當於物理引用,直接引用是可以指向目標的指針、相對偏移量或者是一個能直接或間接定位到目標的句柄。和虛擬機實現的內存有關,不同的虛擬機直接引用一般不同。轉換爲直接引用時,引用的目標必然已經在內存中存在。

注意:
1、虛擬機並沒有規定解析階段開始的具體時間,只是當需要執行某條用於操作符號引用的指令(16種)時,我們要在指令執行之前完成對該符號引用的轉換。因此虛擬機可以根據需要來決定何時來進行符號解析。

2、除了invokeddynamic指令外,虛擬機第一次執行其它指令時,會將其操作的符號引用解析的結果緩存在運行時常量池中,避免重複解析。並且無論真正解析多少次,虛擬機需要保證成功一次之後,接下來的解析都得成功;解析失敗時,其它指令對該符號得解析請求會收到異常。

3、解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符合限引用。分別對應常量池中的七種常量類型。

初始化

初始化階段是通過執行構造器clinit()來進行類變量賦值和靜態代碼塊的執行。虛擬機會保證在子類的構造器clinit()執行之前,父類的構造器clinit()會先得到執行,因此父類的靜態變量賦值語句和靜態代碼塊會在子類的之前執行(執行順序按照代碼順序執行,定義在靜態代碼塊之後的靜態變量,靜態代碼塊只能賦值,不能訪問)。

因爲構造器clinit()是由編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊的語句合併產生的,因此當類中無類變量的賦值動作和靜態代碼塊時,變量不會爲該類生成構造器clinit()。

注意: 在深入理解Java虛擬機這本書上,描述接口雖然不能有靜態代碼塊,但因爲有變量初始化的賦值操作,所以接口也會生成構造器clinit()。
(個人疑惑,接口中的變量是靜態常量,若該常量值是在編譯器就編譯到類的常量池中,則在準備階段就進行了賦值操作,若不是編譯時常量,靜態變量是一個引用,比如一個隨機生成數,那麼應該是在初始化階段進行賦值,然後加入運行時常量池,這是個人的理解,不是特別確定,求大神指點證實一下,萬分感謝!)

public static final int FINAL_VALUE_INT = new Random(66).nextInt();  

三、Java的靜態變量、靜態函數、靜態代碼塊

Java中存在着一種僞全局變量,即爲靜態變量,它與靜態函數、靜態代碼塊一同只屬於類本身,而不屬於類的實例對象,但對象可以訪問類變量,我們可以通過類名或者對象名對它進行訪問或者修改。

靜態變量

靜態變量在類加載的準備階段,進行內存分配,設置默認值值;在類加載的初始化階段進行真正的初始化賦值。

靜態變量是屬於類本身的變量,因此又被稱作類變量,所有的實例共用這一份變量,所有實例都可以訪問或者修改該變量。

子類引用父類的靜態變量,不會造成子類的類加載。

靜態代碼塊

1、靜態代碼塊在類加載的初始化階段開始自動執行,多個靜態代碼塊會按照代碼順序進行順序執行,且只能執行一次。

2、靜態代碼塊對於定義在它後面的靜態變量,只能賦值不能訪問。

3、靜態代碼塊內不能直接訪問非靜態成員變量或者成員方法。非靜態成員變量和方法屬於對象實例,靜態代碼塊在執行時,還未實例化對象。但可通過對象間接訪問非靜態成員變量和方法。(這裏的實例對象可以是類本身的對象,如類A的靜態代碼塊中可以通過類A的實例對象B訪問其非靜態方法或者變量,個人理解,到了初始化這一階段,除了類變量賦值和靜態代碼塊沒執行,其它都已經準備好,因此這裏可以實例化自己的對象)。

4、靜態代碼塊只能定義在類裏面,不能定義在方法裏面。

5、靜態代碼塊裏的變量都是局部變量,只在塊內有效。

6、靜態代碼塊多用於對類的初始化。

    static{
        InheritFoundation  ih= new InheritFoundation ("11","11");
        ih.notAbstract();
    }

靜態方法

1、靜態方法屬於類,在內存中只有一份,所有的實例共用這個方法。

2、靜態方法同靜態代碼塊一樣,只能直接訪問靜態成員變量,不能直接訪問非靜態成員變量和方法,可通過對象間接訪問。

3、靜態方法可通過類名和對象名進行訪問。

4、靜態方法內不能出現this,因爲this關鍵字屬於對象。

5、靜態方法不能被重寫,而是被根據引用類型被調用,表現爲重寫。若子父類都有靜態方法A(),使用子類調用方法A()時,調用子類的靜態方法,使用父類調用方法A()時,調用父類的靜態方法。

6、若是子類調用父類的特有靜態方法,則不會產生子類的類加載。

   public static void  resr (){
       InheritFoundation ih= new InheritFoundation ("22","33");
         ih.notAbstract();
   }

四、執行順序解析

父類靜態方法、靜態代碼塊、代碼塊、構造器以及子類靜態方法、靜態代碼塊、代碼塊、構造器的執行順序如下:

1、先執行父類靜態代碼塊(多個靜態代碼塊按代碼先後順序執行);

2、再執行子類靜態代碼塊(多個靜態代碼塊按代碼先後順序執行);

注: 這兩個步驟是在類加載的初始階段完成。)

3、再執行父類方法塊(方法塊與靜態方法塊的區別是方法塊屬於對象而不是類本身);

4、再執行父類構造器完成父類對象實例化;

5、再執行子類方法塊;

6、最後執行子類構造器,完成子類對象實例化。

重點注意: 子類靜態方法以及父類靜態方法的執行順序是怎樣往往取決於你在哪裏調用該方法。)

7、若你在對象實例化之前,用父類進行調用靜態方法,則它出現在父類靜態代碼塊之後(因爲只有完成了類加載之後,才能調用靜態方法)。

8、若你在對象實例化之前,用子類進行調用靜態方法,則出現在子類靜態代碼塊之後(加載子類,必須加載父類,那父類靜態代碼塊先執行,子類代碼塊後執行,完成加載再執行子類靜態方法)。
(特殊情況: 若子類調用的是父類的特有靜態方法,則不會加載子類,因此發生在父類代碼塊之後。)

9、若在對象實例化之後,則該靜態函數調用會發生在對象構造方法執行之後。
(靜態方法只要在父子類的靜態代碼塊之後即可,因爲需要完成類加載)

代碼示例

public class StaticMethodDemo {                                        
    public static void main(String[] args){                            
        int age=22;                                                    
    StaticMethodTest.ParentUniqueStatic();                             
     Parent.ParentStatic();                                            
    StaticMethodTest.ParentStatic();                                   
     //   System.out.println(""+StaticMethodTest.high);                
    StaticMethodTest test=new StaticMethodTest(22);                    
    // test.high=5;                                                    
    //System.out.println(StaticMethodTest.high);                       
     StaticMethodTest.ParentStatic();                                  
     Parent.ParentStatic();                                            
     StaticMethodTest.ParentUniqueStatic();                            
      test.childMethod();                                              
/*        StaticMethodTest.ParentStatic();                             
        System.out.println();*/                                        
    }                                                                  
}                                                                      
                                                                       
class StaticMethodTest extends Parent{                                 
    private static  String children="children";                        
    public static  int childHigh=4;                                    
    private int childAge;                                              
                                                                       
                                                                       
    public StaticMethodTest(int Age){                                  
        super(Age+1);                                                  
        this.childAge=Age;                                             
        System.out.println("子類構造器");                                   
    }                                                                  
    public static void  ParentStatic(){                                
        System.out.println("子類靜態方法調用");                                
    }                                                                  
    static{                                                            
        System.out.println("子類靜態方法塊調用");                               
    }                                                                  
    static{                                                            
        System.out.println("子類靜態方法塊2調用");                              
    }                                                                  
    {                                                                  
        System.out.println("子類方法塊調用");                                 
    }                                                                  
    {                                                                  
        System.out.println("子類方法塊2調用");                                
    }                                                                  
   /* @Override*/                                                      
    public void childMethod(){                                         
        System.out.println("子類普通方法");                                  
    }                                                                  
                                                                       
}                                                                      
                                                                       
class Parent {                                                         
    private static String parent= "parent";                            
    public  static int high=15;                                        
    private int parentAge;                                             
                                                                       
    public Parent(int i) {                                             
        this.parentAge=i;                                              
        System.out.println("父類構造器");                                   
    }                                                                  
    public static void ParentUniqueStatic(){                           
            System.out.println("父類特有靜態方法調用,子類沒有喲");                    
    }                                                                  
    public static void ParentStatic() {                                
        System.out.println("父類靜態方法調用");                                
    }                                                                  
                                                                       
    static {                                                           
        System.out.println("父類靜態方法塊調用");                               
    }                                                                  
    static {                                                           
        System.out.println("父類靜態方法1塊調用");                              
    }                                                                  
    {                                                                  
        System.out.println("父類方法塊調用");                                 
    }                                                                  
    {                                                                  
        System.out.println("父類方法塊1調用");                                
    }                                                                  
    public void childMethod() {                                        
        System.out.println("父類普通方法");                                  
    }                                                                  
                                                                       
}                                                                      
                                                                       
                                                                       

執行結果:
父類靜態方法塊調用
父類靜態方法1塊調用
父類特有靜態方法調用,子類沒有喲
父類靜態方法調用
子類靜態方法塊調用
子類靜態方法塊2調用
子類靜態方法調用
父類方法塊調用
父類方法塊1調用
父類構造器
子類方法塊調用
子類方法塊2調用
子類構造器
子類靜態方法調用
父類靜態方法調用
父類特有靜態方法調用,子類沒有喲
子類普通方法

五、總結

1、無論是靜態方法還是非靜態方法都是在類加載完成之後才能進行調用。

2、靜態代碼塊和靜態變量的初始化完成是在類加載的最後一個階段初始化階段。靜態代碼塊只執行一次,多用於對類的初始化。

3、靜態方法只是表現爲重寫,其實是根據引用類型不同而進行不同的調用,子類調用,則調用子類的靜態方法,若父類調用則調用父類的靜態方法。

4、子類調用父類的靜態屬性或者父類的特有靜態方法不會造成子類的類加載。

5、執行順序要點:子類加載之前,必須加載父類,因此父類的執行會在子類之前。調用靜態方法或者構造對象實例之前,必須完成類加載,因此靜態代碼塊肯定先執行;普通代碼塊屬於實例,在實例構造器執行之前執行。靜態方法或者非靜態方法執行需要看是誰執行的、在何時執行來決定順序。

6、類加載過程其實是個比較自由的過程,雖然名義上是有着前後順序的五個階段,但其實往往是交叉執行,且往往只規定了某個階段需要在什麼情況下必須觸發完成,而不是需要五個階段必須像流水一樣,一旦開始加載必須依次立馬完成。


最後拋出個個人疑惑,希望能有大神解答下:

疑惑: 在深入理解Java虛擬機這本書上,描述接口雖然不能有靜態代碼塊,但因爲有變量初始化的賦值操作,所以接口也會生成構造器clinit()。
(個人疑惑,接口中的變量是靜態常量,若該常量值是在編譯器就編譯到類的常量池中,則在準備階段就進行了賦值操作,若不是編譯時常量,靜態變量是一個引用,比如一個隨機生成數,那麼應該是在初始化階段進行賦值,然後加入運行時常量池,這是個人的理解,不是特別確定,求大神指點證實一下,萬分感謝!)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章