Java基礎篇—Java類載入機制

singed cat發表於2019-03-31

Java 類載入機制

Java類載入過程

基於 JDK8,另 ==xx== 為高亮操作,高亮 xx,Typora 支援,而掘金不支援)

類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期包括

  • ==載入== Loading
  • 連結 Linking
    • ==驗證== Verification
    • ==準備== Preparation
    • ==解析== Resolution
  • ==初始化== Initialization
  • ==使用== Using
  • ==解除安裝== Unloading

載入驗證準備初始化解除安裝 這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,然後通常互相交叉地混合式進行,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。

載入

載入的過程中主要做了3件事:

  • 通過一個類的全限定名來獲取定義此類的二進位制流
  • 將二進位制流所代表的儲存結構轉化為==方法區==的執行時資料結構。
  • 在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區的各種資料訪問入口。

對於一個類或者介面

  • 如果該類不是陣列類,則使用類載入器載入二進位制表示。
  • 如果該類是陣列類,則由Java虛擬機器建立,因為陣列類不具有外部二進位制表示形式。
類載入器種類

有兩種型別的類載入器:

  • 由 JVM 提供的 ==bootstrap 類載入器==:用於載入 系統變數 sun.boot.class.path 所代表的路徑下的 class 檔案,頂層父類

  • 使用者定義的類載入器(java 類庫中定義的也包括在內)

    • ==ExtClassLoader==:sun.misc.Launcher 類中定義,用於載入 系統變數 java.class.path 所代表的路徑下的 class 檔案;父類載入器為 bootstrap
    • ==AppClassLoader==:sun.misc.Launcher 類中定義,用於載入 系統變數 java.ext.dirs 所代表的 路徑下 class 檔案;父類載入器為 ExtClassLoader
    • 其他,當然你也可以定義其他型別的類載入器
類載入器載入原理

ClassLoader 使用 ==雙親委派模型== 來載入類,每個 ClassLoader 例項都持有父類載入器的引用,虛擬機器內建的 bootstrap 類載入器為頂層父類載入器,沒有父類載入器,但可以作為其它 ClassLoader 例項的父類載入器。當 ClassLoader 例項需要載入某個類時,它會先委派其父類載入器去載入。這個過程是由上至下依次檢查的,首先由最頂層的類載入器Bootstrap ClassLoader試圖載入,如果沒載入到,則把任務轉交給Extension ClassLoader試圖載入,如果也沒載入到,則轉交給App ClassLoader 進行載入,如果它也沒有載入得到的話,則返回給委託的發起者,由它到指定的檔案系統或網路等URL中載入該類。如果它們都沒有載入到這個類時,則丟擲ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,並將它載入到記憶體當中,最後返回這個類在記憶體中的Class例項物件。

雙親委派機制的優點是可以避免類的重複載入,當父類載入了子類就沒必要再載入。另外能夠保證虛擬機器的安全,防止內部實現類被自定義的類替換。

那麼JVM在搜尋類的時候,如何判斷兩個 class 是否相同呢?答案是不僅全類名要相同 ,而且還要由同一個類載入器例項載入。

驗證

主要確保類或介面的二進位制表示在結構上是正確的。

準備

準備工作包括為類或介面建立靜態欄位,並將這些欄位初始化為其預設值,這裡 不需要執行任何 java 程式碼。

例如,對於類或介面中的如下靜態欄位

private static int num = 666;
複製程式碼

在準備階段會為 num 設定預設值 0;在後面的初始化階段才會給 num 賦值 666;

特殊情況:對於同時被 static 和 final 修飾的 欄位,準備階段就會賦值。

解析

解析是將執行常量池中的符號引用 動態確定為具體值的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行。

初始化

初始化階段是初始化類變數和其他資源,或者說是執行類構造器<clinit>()方法的過程.

<clinit>()方法是由編譯器自動收集類中的所有==類變數==(static 修飾的變數)的賦值動作和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。

例如:非法向前引用變數示例

static {
    i = 2;
    System.out.println(i); //illegal forward reference
}
static int i = 4;
複製程式碼

<clinit>()方法與類的建構函式(或者說例項構造器<init>()方法)不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機器中第一個被執行的<clinit>()方法的類肯定是 java.lang.Object。介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法,只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。

<clinit>()方法是類構造器,在類初始化的時候執行,用於 類中的靜態程式碼塊 和 靜態欄位 初始化的方法,只會執行一次。

<init>()方法是 類例項的構造器,在物件的初始化階段執行,用於非靜態欄位,非靜態程式碼塊,建構函式的初始化,可以執行多次。

類或者介面只能由於以下原因初始化:

  1. 使用 new 建立新物件,如果引用的類尚未被初始化,則初始化該類;或者從類中獲取 靜態欄位、設定靜態欄位、執行類中的靜態方法時,如果還沒有被初始化,則 ==宣告該欄位或者方法的類或者介面被初始化==
  2. 第一次呼叫 java.lang.invoke.method handle例項,呼叫了靜態方法,或者 new 的方法的控制程式碼。
  3. 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化
  4. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
  5. 當初始化一個類,而該類直接或者間接 實現了 一個 不含有抽象方法,和靜態方法的介面,則需要初始化該介面。
  6. 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。

牛刀小試1

執行如下程式碼,輸出如何:

public class SSClass
{
    static
    {
        System.out.println("SSClass");
    }
}    
public class SuperClass extends SSClass
{
    static
    {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;

    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
public class SubClass extends SuperClass
{
    static 
    {
        System.out.println("SubClass init");
    }

    static int a;

    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
public class NotInitialization
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}

複製程式碼

輸出:

SSClass
SuperClass init!
123
複製程式碼

解析:

上面提到了這樣一句話:

==從類中獲取 靜態欄位、設定靜態欄位、執行類中的靜態方法時,如果還沒有被初始化,則 宣告該欄位或者方法的類或者介面被初始化==

所以 SubClass 類並不會被初始化,所以也就不會執行其 靜態程式碼塊;

牛刀小試2

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;
}

複製程式碼

問題:執行上面的程式,輸出結果是什麼?

答案:執行以上程式碼的輸出結果:

2
3
a=110,b=0
1
4
複製程式碼

解析:

  • 執行 main 方法,會導致主類 StaticTest 初始化,由於還未載入,先執行載入,主要分析 準備階段 和 初始化階段 的 賦值操作。

  • 準備階段 : 為靜態欄位賦初始值,st 設為 null ,b 設為 0;

  • 初始化階段:執行 Java 程式碼的類構造器 <clinit>()方法,分別按順序執行如下程式碼:

    • static StaticTest st = new StaticTest();new 會 導致 StaticTest 類執行類初始化,執行 <init>()方法,物件的初始化是先初始化成員變數 和 程式碼塊,再載執行構造方法;
      • int a = 110;
      • System.out.println("2");
      • System.out.println("3");
      • System.out.println("a=" + a + ",b=" + b);
    • static 靜態程式碼塊,System.out.println("1");
    • static int b = 112;
  • 呼叫 staticFunction() 方法,System.out.println("4");

稍微修改一下程式碼,去除 以下程式碼,或許就變得正常多了,

static StaticTest st = new StaticTest();
複製程式碼

輸出:

1
4
複製程式碼

減少了 類例項化的步驟。

父類和子類的初始化順序可以簡單用以下幾句話概括:

  • 父類的靜態變數賦值
  • 自身的靜態變數賦值
  • 父類成員變數賦值和父類塊賦值
  • 父類建構函式賦值
  • 自身成員變數賦值和自身塊賦值
  • 自身建構函式賦值

參考:

深入理解Java虛擬機器

相關文章