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

原文:http://blog.csdn.NET/u010425776/article/details/51251430

類的生命週期

一個類從加載進內存到卸載出內存爲止,一共經歷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
  • 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];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

輸出結果: 
並沒有輸出“父類被初始化!” 
原因分析: 
這個過程看似滿足初始化時機的第一條:遇到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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

輸出結果: 
柴毛毛 
原因分析: 
本示例看似滿足類初始化時機的第一個條件:獲取一個類靜態成員變量的時候若類尚未初始化則初始化類。 
但是,Fu類的靜態成員變量被final修飾,它已經是一個常量。被final修飾的常量在Java代碼編譯的過程中就會被放入它被引用的class文件的常量池中(這裏是A的常量池)。所以程序在運行期間如果需要調用這個常量,直接去當前類的常量池中取,而不需要初始化這個類。 

5. 接口的初始化

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

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