原文:http://blog.csdn.NET/u010425776/article/details/51251430
類的生命週期
一個類從加載進內存到卸載出內存爲止,一共經歷7個階段:
加載——>驗證——>準備——>解析——>初始化——>使用——>卸載
其中,類加載包括5個階段:
加載——>驗證——>準備——>解析——>初始化
在類加載的過程中,以下3個過程稱爲連接:
驗證——>準備——>解析
因此,JVM的類加載過程也可以概括爲3個過程:
加載——>連接——>初始化
C/C++在運行前需要完成預處理、編譯、彙編、鏈接;而在Java中,類加載(加載、連接、初始化)是在程序運行期間完成的。
在程序運行期間進行類加載會稍微增加程序的開銷,但隨之會帶來更大的好處——提高程序的靈活性。Java語言的靈活性體現在它可以在運行期間動態擴展,所謂動態擴展就是在運行期間動態加載和動態連接。
類加載的時機
1. 類加載過程中每個步驟的順序
我們已經知道,類加載的過程包括:加載、連接、初始化,連接又分爲:驗證、準備、解析,所以說類加載一共分爲5步:加載、驗證、準備、解析、初始化。
其中加載、驗證、準備、初始化的開始順序是依次進行的,這些步驟開始之後的過程可能會有重疊。
而解析過程會發生在初始化過程中。
2. 類加載過程中“初始化”開始的時機
JVM規範中只定義了類加載過程中初始化過程開始的時機,加載、連接過程都應該在初始化之前開始(解析除外),這些過程具體在何時開始,JVM規範並沒有定義,不同的虛擬機可以根據具體的需求自定義。
初始化開始的時機:
- 在運行過程中遇到如下字節碼指令時,如果類尚未初始化,那就要進行初始化:new、getstatic、putstatic、invokestatic。這四個指令對應的Java代碼場景是:
- 通過new創建對象;
- 讀取、設置一個類的靜態成員變量(不包括final修飾的靜態變量);
- 調用一個類的靜態成員函數。
- 使用java.lang.reflect進行反射調用的時候,如果類沒有初始化,那就需要初始化;
- 當初始化一個類的時候,若其父類尚未初始化,那就先要讓其父類初始化,然後再初始化本類;
- 當虛擬機啓動時,虛擬機會首先初始化帶有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. 接口的初始化
接口和類都需要初始化,接口和類的初始化過程基本一樣,不同點在於:類初始化時,如果發現父類尚未被初始化,則先要初始化父類,然後再初始化自己;但接口初始化時,並不要求父接口已經全部初始化,只有程序在運行過程中用到當父接口中的東西時才初始化父接口。