類加載機制及類加載順序

類加載機制概述:

     虛擬機把描述類的數據從Classe文件加載到內存,並對數據進行檢驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類加載的時機:

  1. 類加載的生命週期:加載、連接(驗證、準備、解析)、初始化、使用和卸載
  2. 加載時機:Java虛擬機規範中並沒有進行強制約束,由虛擬機自由把握。
  3. 連接時機:加載之後就開始連接,加載結束連接才結束。
  4. 初始化時機:Java虛擬機規範中嚴格規定有且只有5種情況立即對類進行“初始化”:
      1).遇到new、getstatic、puttstatic或invoketstatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令的常用場景:使用new關鍵字實例化對象時讀取或設置類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)時、調用類靜態方法的時候
      2).使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。
      3).初始化一個類的時候,如果發現其父類沒有進行初始化,則需要先觸發父類的初始化。
      4).虛擬機啓動時,需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個類。
加載:

     加載階段主要完成3件事:

  1. 通過一個類的完全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構。
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類各種數據的訪問入口。
         
驗證:

     這一階段主要是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並不會危害虛擬機自身安全。該階段大致會完成4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

準備:

     該階段正式爲類變量分配內存並設置類變量(static修飾的變量)的初始值(默認值,比如int類型爲0,Boolean爲false… …),但如果被final修飾的類變量,初始值即爲常量值,比如:

public static final int a =123; // 準備階段會將a設置爲123

     該階段不爲實例變量分配內存,實例變量會在對象實例化時隨對象一起分配到Java堆中。

解析:

     該階段是虛擬機將常量池的符號引用替換爲直接引用的過程。

初始化:

     該階段是執行類構造器初始化方法< clinit>()的過程。
     1).< clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併產生的;編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,在前面的靜態語句塊可以賦值,但不能訪問。

public class Test {
    static {
        i = 0;                       //給變量賦值可以正常編譯通過
        System.out.print(i);         //編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
//靜態語句塊可以給塊後定義的靜態變量賦值 i=0;但不能引用,即第4行會提示錯誤

     2).< clinit>()方法與類的構造函數(或者說實例構造器< init>方法)不同,虛擬機會保證在子類的初始化< clinit>()方法執行之前,父類的初始化方法已經執行完畢。
     3).由於父類的< clinit>()方法先執行,意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作

static class Parent {
    public static int A = 1;
    static {
        A = 2;
   }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
    System.out.println(Sub.B);
}
//父類中定義的靜態語句塊先執行,所以結果爲2.

     4).< clinit>()方法對於類或接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不爲這個類生成< clinit>()方法。
     5).接口中不能使用靜態語句塊,但仍然有變量初始化的操作。
     6).虛擬機會保證一個類的< clinit>()方法在多線程環境中被正確的加鎖、同步;如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的< clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行類初始化方法完畢。

舉個栗子:

     父類:

package Test_ClassLoard;
public class Person {

    public static int a = 10;
    public String b = "aaa";
    
    public Person() {System.out.println("--父類構造方法--");}

    static {
        System.out.println("--父類靜態屬性a:"+a);
        System.out.println("--父類靜態代碼塊--");
    }

    {
        System.out.println("--父類成員變量b:"+b);
        System.out.println("--父類非靜態代碼塊--");
    }

    private static void staticMethod(){System.out.println("--父類靜態方法--");}

    private void noStaticMethod(){ System.out.println("--父類非靜態方法--");}
}

     子類:

package Test_ClassLoard;
public class Children extends Person{

    public static final int c = 100;
    private String d = "bbb";

    public Children() { System.out.println("--子類構造方法--");}

    static {
        System.out.println("--子類靜態屬性c:"+c);
        System.out.println("--子類靜態代碼塊--");
    }

    {
        System.out.println("--子類成員變量d:"+d);
        System.out.println("--子類非靜態代碼塊--");
    }

    public static void staticMethod(){System.out.println("--子類靜態方法--");}

    private void noStaticMethod(){ System.out.println("--子類非靜態方法--");}
}

     入口類:

package Test_ClassLoard;
public class App {
    public static void main(String[] args) {
        Children children = new Children();
        System.out.println("----------");
        new Children();
//        System.out.println(Children.c);//被final修飾,類加載時不會初始化
//        System.out.println(Children.a);//子類調用父類類變量,子類加載時不會初始化
    }
}

     運行結果:
在這裏插入圖片描述
     過程:
     1). 編譯好 App.java 後得到 App.class 後,執行 App.class,系統會啓動一個 JVM 進程,從 classpath 路徑中找到一個名爲 App.class 的二進制文件,將 App 的類信息加載到運行時數據區的方法區內(類加載)。
     2). JVM 找到 App 的主程序入口,執行main方法。
     3). 自上而下執行,遇到new指令,發現方法區的運行時常量池沒有Children類的符號引用(參考對象的創建),則先加載Children類(可見類加載是懶加載);由於繼承父類,所以會先加載父類Person(加載、驗證、準備、解析),然後對Person進行初始化(執行類構造器初始化方法< clinit>() –自上而下執行類變量和靜態語句塊在這裏插入圖片描述
     4).Person類加載完後同理加載子類Children,輸出:
          –子類靜態屬性c:100
          –子類靜態代碼塊–
     5).加載類後,虛擬機爲父類Person分配內存,然後調用< init >()構造函數初始化 Person實例。同理,再給子類Children分配內存和< init >()初始化。所以輸出:
          –父類成員變量b:aaa
          –父類非靜態代碼塊–
          –父類構造方法–
          –子類成員變量d:bbb
          –子類非靜態代碼塊–
          –子類構造方法–
     6).至此, Children children = new Children();新建對象完畢;當再次 new Children()時,之前父類和子類都已經加載過了,所以只輸出實例化信息。

     7). 小結: 類變量和靜態代碼塊,在類加載的時候自上而下執行;
                     成員變量和非靜態代碼塊在類實例化時自上而下執行;類方法在調用時自上而下執行;
                     無論是類加載還是實例化都先執行父類再執行子類;
                     先加載再實例化;

     參考:
    《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版)》
     大部分內容均來自這本書,下圖爲思維導圖。
在這裏插入圖片描述
     互相交流,互相學習,有誤指正。

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