天天看點

幾套筆試題下來,終于搞清楚了Java靜态代碼塊-普通代碼塊-構造方法的執行順序

目錄

驗證

為什麼靜态塊先執行,在什麼時候執行

JVM類加載過程

靜态代碼塊執行

為什麼普通代碼塊優于構造方法執行

誤區探究:靜态代碼塊,會在類被加載時自動執行嗎

【結論】

靜态代碼塊 ==> main方法 ==> 普通代碼塊 ==> 構造方法

  • 如果有繼承關系,父類先由于子類執行
  • 靜态代碼塊隻在類加載時執行一次(後面再執行個體化對象時,該類已經加載過,是以靜态塊不再執行)
幾套筆試題下來,終于搞清楚了Java靜态代碼塊-普通代碼塊-構造方法的執行順序

驗證

class MyClass {
    public MyClass() {
        System.out.println("父類構造方法");
    }

    static {
        System.out.println("父類靜态代碼塊");
    }

    {
        System.out.println("父類非靜态代碼塊");
    }

    public static void method() {
        System.out.println("父類普通方法執行");
    }
}

public class One extends MyClass{
    public One() {
        System.out.println("子類構造方法");
    }

    static {
        System.out.println("子類靜态代碼塊");
    }

    {
        System.out.println("子類非靜态代碼塊");
    }
    public static void method() {
        System.out.println("子類普通方法執行");
    }


    public static void main(String[] args) {
        System.out.println("子類main方法執行");

        MyClass once = new One();
        once.method();

        System.out.println("------------");
        
         //第二次類加載, 父類和子類得靜态代碼塊不再執行
        MyClass twice = new One();
        twice.method();
    }
}
           

執行結果:

  1. 父類靜态代碼塊
  2. 子類靜态代碼塊
  3. 子類main方法執行
  4. 父類非靜态代碼塊
  5. 父類構造方法
  6. 子類非靜态代碼塊
  7. 子類構造方法
  8. 父類普通方法執行
  9. ------------------------ 第二次執行個體化對象,父類和子類的靜态代碼塊不再執行
  10. 父類非靜态代碼塊
  11. 父類構造方法
  12. 子類非靜态代碼塊
  13. 子類構造方法
  14. 父類普通方法執行
幾套筆試題下來,終于搞清楚了Java靜态代碼塊-普通代碼塊-構造方法的執行順序

為什麼靜态塊先執行,在什麼時候執行

JVM類加載過程

首先,得從JVM類加載器說起。可以參考我之前寫過的文章

🔗【探究JVM一】JVM的入口——類加載器子系統

一個類從 .java 到 .class 加載到JVM解釋執行,需要經曆三大步驟:

幾套筆試題下來,終于搞清楚了Java靜态代碼塊-普通代碼塊-構造方法的執行順序

加載 ==> 連結 ==> 初始化

其中裝載階段又三個基本動作組成:

  1.     通過類型的完全限定名,産生一個代表該類型的二進制資料流
  2.     解析這個二進制資料流為方法區内的内部資料結
  3.     建構立一個表示該類型的java.lang.Class類的執行個體

另外如果一個類裝載器在預先裝載的時遇到缺失或錯誤的class檔案,它需要等到程式首次主動使用該類時才報告錯誤。

連結階段又分為三部分:

  1. 驗證,确認類型符合Java語言的語義,檢查各個類之間的二進制相容性(比如final的類不用擁有子類等),另外還需要進行符号引用的驗證。
  2. 準備,Java虛拟機為類變量配置設定記憶體,設定預設初始值。
  3. 解析(可選的) ,在類型的常量池中尋找類,接口,字段和方法的符号引用,把這些符号引用替換成直接引用的過程。

當一個類被主動使用時,JVM就會對其初始化,如下六種情況為主動使用:

  1. 當建立某個類的新執行個體時(如通過new或者反射,克隆,反序列化等)
  2. 當調用某個類的靜态方法時
  3. 當使用某個類或接口的靜态字段時
  4. 當調用Java API中的某些反射方法時,比如類Class中的方法,或者java.lang.reflect中的類的方法時
  5. 當初始化某個子類時
  6. 當虛拟機啟動某個被标明為啟動類的類(即包含main方法的那個類)
  7. JDK1.7開始提供的動态語言支援,java.lang.invoke.MethodHandle執行個體的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic句柄對應的類沒有初始化,則初始

    被動使用

除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導緻類的初始化

Java編譯器會收集所有的類變量初始化語句和類型的靜态初始化器,将這些放到一個特殊的<clinit>方法中

靜态代碼塊執行

實際上,static塊的執行發生在“初始化”的階段。初始化階段,JVM主要完成對靜态變量的初始化,靜态塊執行等工作。

下面我們看看執行static塊的幾種情況:

  1. 第一次new A()的過程會列印"";因為這個過程包括了初始化
  2. 第一次Class.forName("A")的過程會列印"";因為這個過程相當于Class.forName("A",true,this.getClass().getClassLoader());
  3. 第一次Class.forName("A",false,this.getClass().getClassLoader())的過程則不會列印""。因為false指明了裝載類的過程中,不進行初始化。不初始化則不會執行static塊。

為什麼普通代碼塊優于構造方法執行

幾套筆試題下來,終于搞清楚了Java靜态代碼塊-普通代碼塊-構造方法的執行順序

通過檢視編譯完成的 .class檔案 發現,普通代碼塊實際上是被放到了構造方法中,且是放在了構造方法的第一句

那麼就不難解釋:為什麼普通代碼塊會比構造方法執行順序考前了。

在執行子類的構造方法時,子類 super 指向父類的構造方法會先執行父類的構造方法。由于靜态代碼塊執行是優先于main方法的,是以先回去執行父類的靜态代碼塊再執行子類的靜态代碼塊。此時 super 由于指向父類需要去執行父類的構造方法,且普通代碼塊會被轉換到構造方法的第一行,是以此時就會執行父類的代碼塊以及構造方法。當super執行完畢回到子類時,由于子類的代碼塊也被放到了構造方法中,且在super之後是以執行子類代碼塊再執行子類構造方法。

因為 main 方法池Java程式執行的入口,是以優于構造方法和普通代碼塊的執行;但是,靜态塊是在類加載時的初始化階段就執行了,此時 main方法還沒有進入到虛拟機棧中,是以是靜态代碼塊最先執行。

【文章參考】

[1]  [美] 文納斯. 《深入Java虛拟機》第二版

[2] oracle docs. https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#forName%28java.lang.String,%20boolean,%20java.lang.ClassLoader%29

[3] oracle docs. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html