天天看點

深入了解JVM(八)——類加載的時機

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/qq_34173549/article/details/79612705

類的生命周期

一個類從加載進記憶體到解除安裝出記憶體為止,一共經曆7個階段: 

加載——>驗證——>準備——>解析——>初始化——>使用——>解除安裝

其中,類加載包括5個階段: 

加載——>驗證——>準備——>解析——>初始化

在類加載的過程中,以下3個過程稱為連接配接: 

驗證——>準備——>解析

是以,JVM的類加載過程也可以概括為3個過程: 

加載——>連接配接——>初始化

C/C++在運作前需要完成預處理、編譯、彙編、連結;而在Java中,類加載(加載、連接配接、初始化)是在程式運作期間完成的。 

在程式運作期間進行類加載會稍微增加程式的開銷,但随之會帶來更大的好處——提高程式的靈活性。Java語言的靈活性展現在它可以在運作期間動态擴充,所謂動态擴充就是在運作期間動态加載和動态連接配接。 

類加載的時機

1. 類加載過程中每個步驟的順序

我們已經知道,類加載的過程包括:加載、連接配接、初始化,連接配接又分為:驗證、準備、解析,是以說類加載一共分為5步:加載、驗證、準備、解析、初始化。

其中加載、驗證、準備、初始化的開始順序是依次進行的,這些步驟開始之後的過程可能會有重疊。 

而解析過程會發生在初始化過程中。 

2. 類加載過程中“初始化”開始的時機

JVM規範中隻定義了類加載過程中初始化過程開始的時機,加載、連接配接過程都應該在初始化之前開始(解析除外),這些過程具體在何時開始,JVM規範并沒有定義,不同的虛拟機可以根據具體的需求自定義。

初始化開始的時機:

  1. 在運作過程中遇到如下位元組碼指令時,如果類尚未初始化,那就要進行初始化:new、getstatic、putstatic、invokestatic。這四個指令對應的Java代碼場景是: 
    • 通過new建立對象;
    • 讀取、設定一個類的靜态成員變量(不包括final修飾的靜态變量);
    • 調用一個類的靜态成員函數。
  2. 使用java.lang.reflect進行反射調用的時候,如果類沒有初始化,那就需要初始化;
  3. 當初始化一個類的時候,若其父類尚未初始化,那就先要讓其父類初始化,然後再初始化本類;
  4. 當虛拟機啟動時,虛拟機會首先初始化帶有main方法的類,即主類; 

3. 主動引用 與 被動引用

JVM規範中要求在程式運作過程中,“當且僅當”出現上述4個條件之一的情況才會初始化一個類。如果間接滿足上述初始化條件是不會初始化類的。 

其中,直接滿足上述初始化條件的情況叫做主動引用;間接滿足上述初始化過程的情況叫做被動引用。 

那麼,隻有當程式在運作過程中滿足主動引用的時候才會初始化一個類,若滿足被動引用就不會初始化一個類。 

4. 被動引用的場景示例

  • 示例一
public class Fu{
    public static String name = "柴毛毛";
    static{
        System.out.println("父類被初始化!");
    }
}

public class Zi{
    static{
        System.out.println("子類被初始化!");
    }
}

public static void main(String[] args){
    System.out.println(Zi.name);
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

輸出結果: 

父類被初始化! 

柴毛毛 

原因分析: 

本示例看似滿足初始化時機的第一條:當要擷取某一個類的靜态成員變量的時候如果該類尚未初始化,則對該類進行初始化。 

但由于這個靜态成員變量屬于Fu類,Zi類隻是間接調用Fu類中的靜态成員變量,是以Zi類調用name屬性屬于間接引用,而Fu類調用name屬性屬于直接引用,由于JVM隻初始化直接引用的類,是以隻有Fu類被初始化。 

  • 示例二
public class A{
    public static void main(String[] args){
        Fu[] arr = new Fu[10];
    }
}           

并沒有輸出“父類被初始化!” 

這個過程看似滿足初始化時機的第一條:遇到new建立對象時若類沒被初始化,則初始化該類。 

但現在通過new要建立的是一個數組對象,而非Fu類對象,是以也屬于間接引用,不會初始化Fu類。 

  • 示例三
public class Fu{
    public static final String name = "柴毛毛";
    static{
        System.out.println("父類被初始化!");
    }
}

public class A{
    public static void main(String[] args){
        System.out.println(Fu.name);
    }
}           

本示例看似滿足類初始化時機的第一個條件:擷取一個類靜态成員變量的時候若類尚未初始化則初始化類。 

但是,Fu類的靜态成員變量被final修飾,它已經是一個常量。被final修飾的常量在Java代碼編譯的過程中就會被放入它被引用的class檔案的常量池中(這裡是A的常量池)。是以程式在運作期間如果需要調用這個常量,直接去目前類的常量池中取,而不需要初始化這個類。 

5. 接口的初始化

接口和類都需要初始化,接口和類的初始化過程基本一樣,不同點在于:類初始化時,如果發現父類尚未被初始化,則先要初始化父類,然後再初始化自己;但接口初始化時,并不要求父接口已經全部初始化,隻有程式在運作過程中用到當父接口中的東西時才初始化父接口。