Java 基礎 —— 類加載

類初始化基本知識java

JVM 和類

當使用 java 命令運行 Java 程序時,會啓動一個 Java 虛擬機進程。同一個 JVM 的全部線程、全部變量都處於同一個進程裏,他們都使用該 JVM 進程的內存區。當系統出現以下狀況時,JVM 進程將被終止。面試

  • 程序運行到最後正常結束
  • 程序使用了 System.exit()Runtime.getRuntime().exit()
  • 程序遇到未捕獲的異常或錯誤
  • 程序所在平臺強制結束了 JVM 進程

兩個運行的 Java 程序處於兩個不一樣的 JVM 進程中,兩個 JVM 之間並不會共享數據。緩存

類的加載

當程序主動使用某個類時,若是該類還未被加載到內存中,則系統會經過加載、鏈接、初始化三個步驟來進行該類的初始化。這三個步驟統稱爲「類加載」或「類初始化」。網絡

「類加載」指的是將類的 class 文件讀入內存,併爲之建立一個 java.lang.Class 對象。換言之,程序中使用任何類時,系統都會爲之創建一個 java.lang.Class 對象。jvm

系統中全部的類實際上也是實例,它們都是 java.lang.Class 的實例。測試

類的加載由類加載器完成,類加載器由 JVM 提供。除此以外,開發者能夠經過集成 ClassLoader 基類來自定義類加載。**類加載器一般無須等到」首次使用「該類時才加載它,Java 虛擬機規範容許系統預先加載某些類。this

類的鏈接

類被加載後,系統會爲之生成對應的 Class 對象,接着就會進入鏈接階段。鏈接階段負責把類的二進制數據合併到 JRE 中。類接連分爲以下三個階段:線程

  1. 驗證:驗證階段用於檢驗被加載的類是否有正確的內部結構
  2. 準備:類準備階段則負責爲類的類變量分配內存,並設置默認初始值
  3. 解析:將類的二進制數據中的符號引用替換成直接引用

類的初始化

類初始化階段主要就是虛擬機堆類變量進行初始化。在 Java 類中堆類變量指定初始值有兩種方法:code

  1. 聲明類變量時指定初始值
  2. 使用靜態初始化塊爲類變量指定初始值

若是類變量沒有指定初始值,則採用默認初始值對象

{% note success no-icon %}

  • 靜態初始化塊只會被執行一次(第一次加載該類時),靜態初始化塊先於構造器執行。
  • 類初始化塊和類變量所指定的初始值都是該類的初始化代碼,它們的執行順序與源程序中的排列順序相同。

{% endnote%}

JVM 初始化一個類包含以下幾個步驟:

  1. 加入該類未被加載和鏈接,則程序先加載並鏈接該類
  2. 假如該類的直接父類尚未被初始化,則先初始化其直接父類
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句

第 2 個步驟中,若是直接父類又有父類,會再次重複這三個步驟

實例初始化塊負責對象執行初始化,而類初始化塊是類相關的,系統在類初始化階段執行,而不是在建立對象時才執行。所以,類初始化塊老是比實例初始化塊先執行。只有當類初始化完成以後,才能夠在系統中使用這個類,包括訪問類的類方法、類變量或者用這個類來建立實例。

栗子

Root.java:

public class Root {
    static {
        int root = 1;
        System.out.println("Root 的類初始化塊");
    }

    {
        System.out.println("Root 的實例初始化塊");
    }

    public Root() {
        System.out.println("Root 的無參構造器");
    }
}

Mid.java:

public class Mid extends Root {
    static {
        int mid = 2;
        System.out.println("Mid 的類初始化塊");
    }

    {
        System.out.println("Mid 的實例初始化塊");
    }

    public Mid() {
        System.out.println("Mid 的無參構造器");
    }

    public Mid(String msg) {
        this();
        System.out.println("Mid 的有參構造器,其參數值:" + msg);
    }
}

Leaf.java:

public class Leaf extends Mid {
    static {
        int leaf = 3;
        System.out.println("Leaf 的類初始化塊");
    }

    {
        System.out.println("Leaf 的實例初始化塊");
    }

    public Leaf() {
        super("初始化測試");
        System.out.println("執行Leaf的構造器");
    }
}

Test.java:

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

運行結果會是怎樣的呢?停下來想想。

輸出結果:

Root 的類初始化塊
Mid 的類初始化塊
Leaf 的類初始化塊
Root 的實例初始化塊
Root 的無參構造器
Mid 的實例初始化塊
Mid 的有參構造器,其參數值:初始化測試
Leaf 的實例初始化塊
執行Leaf的構造器
Root 的實例初始化塊
Root 的無參構造器
Mid 的實例初始化塊
Mid 的有參構造器,其參數值:初始化測試
Leaf 的實例初始化塊
執行Leaf的構造器

說明:

  • 優先進行類初始化塊(類靜態塊)的初始化,若是有父類,那麼就先進行父類的的類初始化塊運行;
  • 類初始化塊執行完成以後,會進行實例初始化塊和構造器,若是有父類,則也須要先進行父類的實例初始化塊、構造器執行;

實例初始化塊就是指沒有 static 修飾的初始化塊。當建立該類的 Java 對象時,系統老是先調用該類定義的實例初始化塊(固然,類初始化要已經先完成)。實例初始化是在建立 Java 對象時隱式執行的,並且,在構造器執行以前自動執行

實例初始化栗子

public class InstanceTest {
    {
        a = 1;
    }

    int a = 2;

    public static void main(String[] args) {
        // 輸出 2
        System.out.println(new InstanceTest().a);
    }
}

若是上面例子,將實例初始化塊和實例變量聲明順序調換,輸出就會變爲 1。

建立 Java 對象時,系統先爲該對象的全部實例變量分配內存(前提是該類已被加載過),接着程序對這些實例變量進行初始化:先執行實例初始化塊或聲明實例變量時指定的初始值(按照它們在源碼中的前後順序賦值),而後再執行構造器裏指定的初始值。

{% note success no-icon %}

實際上實例初始化塊是一個假象,使用 javac 命令編譯 Java 類後,該 Java 類中的實例初始化塊會消失—實例初始化塊中代碼會被「還原」到每一個構造器中,且位於構造器全部代碼的前面。

{% endnote%}

類初始化的時機

Java 程序首次經過下面 6 種方式使用某個類或接口時,系統就會初始化該類或接口:

  1. 建立類的實例。包括使用 new 操做符來建立實例、經過反射建立實例、經過反序列化建立實例
  2. 調用某個類的類方法(靜態方法)
  3. 調用某個類的類變量,或爲該類變量賦值
  4. 使用反射方式來強制建立某個類或接口對應的 java.lang.Class 對象。例如 Class.forName("Person")
  5. 初始化某個類的子類。(就是前面介紹過的,該子類的全部父類都會被初始化)
  6. 直接使用 java.exe 命令運行某個主類

{% note warning no-icon %}
對於 final 型的類變量,若是該類變量的值在編譯時就肯定了,那麼,這個類變量至關於「宏變量」。Java 編譯器會在編譯時直接將該類變量出現的地方替換爲它實際的值。所以,程序使用這種靜態變量不會致使該類的初始化。
{% endnote %}

栗子:

class MyTest {
    static {
        System.out.println("靜態初始化塊");
    }

    static final String compileConstant = "類初始化 demo";
}

public class ComileConstantTest {
    public static void main(String[] args) {
        System.out.println(MyTest.compileConstant);
    }
}

輸出:

類初始化 demo

因而可知,的確沒有初始化 MyTest 類。

當類變量使用了 final 修飾,而且,它的值在編譯時就能肯定,那麼它的值在編譯時就肯定了,程序中使用它的地方至關於使用了常量。

若是上面栗子中代碼改成以下:

static final String compileConstant = System.currentTimeMillis() + "";

這時候輸出就是:

靜態初始化塊
1596804413248

由於上面 compileConstant 修改以後,它的值必須在運行時才能肯定,所以,觸發了 MyTestg 類的初始化。

此外,ClassLoader 類的 loadClass() 方法來加載某個類時,該方法只是加載類,並不會執行類的初始化。使用 Class.forName() 靜態方法再回強制初始化類。

栗子:

package class_load;

/**
 * description:
 *
 * @author Michael
 * @date 2020/8/7
 * @time 8:54 下午
 */
class Tester {
    static {
        System.out.println("Tester 類的靜態初始化塊");
    }
}

public class ClassLoadTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        cl.loadClass("class_load.Tester");
        System.out.println("系統加載 Tester 類");
        Class.forName("class_load.Tester");
    }
}

輸出:

系統加載 Tester 類
Tester 類的靜態初始化塊

經測試能夠發現,loadClass 方法確實沒有觸發類的初始化,而 Class.forName 則會初始化 Tester 類。

類加載器

類加載器負責將 .class 文件(可能在磁盤上,也可能在網絡上)加載到內存中,併爲之生成 java.lang.Class 對象。

類加載機制

類加載器負責加載全部的類,系統爲全部被載入內存中的類生成一個 java.lang.Class 對象/實例。一旦一個類被載入 JVM 中,同一個類就不會再次被載入。正是由於有這樣的緩存機制存在,因此 Class 修改以後,必須重啓 JVM 修改纔會生效。

類加載器加載 Class 大體通過以下步驟:

類加載機制

開發者也能夠經過繼承 ClassLoader 來自定義類加載器。由於暫時未涉及這塊,本文暫且略過。

總結

本文重點是瞭解了類初始化的流程,同時,也結合栗子比較了與實例初始化的區別。類初始化塊、實例初始化塊、構造器的執行順序也是面試題常考的內容。最後補充了類加載機制的內容,暫時僅是瞭解。

繪圖採用的 ProcessOn 在線繪製,安利~


生命不息,折騰不止!關注 「Coder 魔法院」,祝你 Niubilitiy !🐂🍺

參考

  • 《瘋狂 Java 講義》第四版,18 章

公衆號-二維碼-截圖

相關文章
相關標籤/搜索