虛擬機類加載-靜態塊順序

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載驗證準備解析初始化使用卸載 7個階段。其中驗證、準備、解析3個部分統稱爲連接。發生順序如下:

這裏寫圖片描述

對於加載,java 虛擬機規範中沒有進行強制約束,交給虛擬機的具體實現來自由把握。但對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種 情況必須立即進行“初始化”(而加載、驗證、準備自然在此之前開始):

1 ) 遇 到 new 、 getstatic 、 putstatic 或 invokestatic 這 4 條 字 節 碼 指 令 時 , 如 果 類 沒 有 進 行 過 初 始 化 , 則 需 要 先 觸 發 其 初 始 化 。 生 成 這 4 條 指 令 的 最 常 見 的 Java 代 碼 場 景 是 : 使 用 new 關 鍵 字 實 例 化 對 象 的 時 候 、 讀 取 或 設 置 一 個 類 的 靜 態 字 段 ( 被 . final 修 飾 、 已 在 編 譯 期 把 結 果
放 人 常 量 池 的 靜 態 字 段 除 外 ) 的 時 候 , 以 及 凋 用 一 個 類 的 靜 態 方 法 的 時 候 。
2 ) 使 用 java-lang 、 reflect 包 的 方 法 對 類 進 行 反 射 調 用 的 時 候 , 如 果 類 沒 有 進 行 過 初 始 化 , 則 需 要 先 觸 發 其 初 始 化 。
3 ) 當 初 始 化 一 個 類 的 時 候 , 如 果 發 現 其 父 類 還 沒 有 進 行 過 初 始 化 , 則 需 要 先 觸 發 其 父 類 的 初 始 化 。
4 ) 當 虛 擬 機 啓 動 時 , 用 戶 需 要 指 定 一 個 要 執 行 的 主 類 ( 包 含 main() 方 法 的 那 個 類 ) , 虛 擬 機 會 先 初 始 化 這 個 主 類 。
5 ) 當 使 用 JDK 1.7 的 動 態 語 言 支 持 時 , 如 果 一 個 java.lang.invokeMethodHandle 實 例 最 後 的 解 析 結 果 REF-getStatic 、 REF_putStatic 、 REF invokeStatic 的 方 法 句 柄 , 並 且 這 個 方 法 句 柄 所 對 應 的 類 沒 有 進 行 過 初 始 化 , 則 需 要 先 觸 發 其 初 始 化

對於這5種會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:“有且只有”,這5個場景中的行爲稱爲對一個類進行主動引用,除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。

例一:

  • 1.通過子類引用父類的靜態字段、不會導致子類初始化

父類:

package com.xnccs.cn.test;

public class SuperClass {

    static{
        System.out.println("SuperClass init!");
    }


    public static int value= 123;
}

子類:

package com.xnccs.cn.test;

public class SubClass extends SuperClass{

    static{
        System.out.println("SubClass init!");
    }
}

測試類:

package com.xnccs.cn.test;




/**
 * 非主動使用
 * @author j_nan
 * 
 * 可通過 -XX:+TraceClassLoading 參數觀察到此操作會導致子類的加載
 *
 */
public class NotInitialization {

    public static void main(String[] args) {
        System.out.print(SubClass.value);
    }

}

輸出:

SuperClass init!
123

結果並沒有打印輸出“SubClass init!” 說明通過子類引用父類的靜態字段、不會導致子類初始化

例二:

  • 通過數組定義來引用類,不會觸發此類的初始化

修改下測試類

package com.xnccs.cn.test;




/**
 * 非主動使用
 * @author j_nan
 * 
 * 可通過 -XX:+TraceClassLoading 參數觀察到此操作會導致子類的加載
 *
 */
public class NotInitialization {

    public static void main(String[] args) {
        SuperClass[] su = new SuperClass[10];

    }

}

輸出:

並沒有輸出SuperClass init! 說明 通過數組定義來引用類,不會觸發此類的初始化

例三:

  • 常量在編譯階段會存入類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
package com.xnccs.cn.test;

public class ConstClass {

    static{
        System.out.println("ConstClass init!");
    }

    public  static final String HELLOWORlD = "hello world";

}


package com.xnccs.cn.test;




/**
 * 非主動使用
 * @author j_nan
 * 
 * 可通過 -XX:+TraceClassLoading 參數觀察到此操作會導致子類的加載
 *
 */
public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }

}

輸出:

hello world

並沒有打印輸出ConstClass init! 我們再用javap 命令看下編譯後的class 文件

這裏寫圖片描述

如上所述,常量在編譯階段會存入調用用的常量池中,也確實在Constant pool 中,也就是說實際上NotInitialization 的Class 文件之中並沒有ConstClass類的符號引用入口,這兩個類在編譯成Class之後就不存在任何聯繫了。

下面再看一個試題,來便於初步理解類加載過程:


package com.xnccs.cn.share;


/**
 * 1.加載的順序:先父類的static成員變量 -> 子類的static成員變量 -> 父類的成員變量 -> 父類構造 -> 子類成員變量 -> 子類構造

    2.static只會加載一次,所以通俗點講第一次new的時候,所有的static都先會被全部載入(以後再有new都會忽略),進行默認初始化。在從上往下進行顯示初始化。這裏靜態代碼塊和靜態成員變量沒有先後之分,誰在上,誰就先初始化

    3.構造代碼塊是什麼?把所有構造方法中相同的內容抽取出來,定義到構造代碼塊中,將來在調用構造方法的時候,會去自動調用構造代碼塊。構造代碼快優先於構造方法。
 * @author j_nan
 *
 */
public class StaticTest {  

    public static int k = 0;  
    public static StaticTest t1 = new StaticTest("t1");  
    public static StaticTest t2 = new StaticTest("t2");  
    public static int i = print("i");  
    public static int n = 99;  
    public int j = print("j");  

    {  
        print("構造塊");  
    }  

    static{  
        print("靜態塊");  
    }  

    public StaticTest(String str) {  
        System.out.println("StaticTest 構造方法:  "+(++k) + ":" + str + " i=" + i + " n=" + n);  
        ++n;  
        ++i;  
    }  

    public static int print(String str) {  
        System.out.println("print 打印: "+(++k) + ":" + str + " i=" + i + " n=" + n);  
        ++i;  
        return ++n;  
    }  
    public static void main(String[] args) {  
        StaticTest t = new StaticTest("init");  
    }  

} 

輸出:

print 打印: 1:j i=0 n=0
print 打印: 2:構造塊 i=1 n=1
StaticTest 構造方法:  3:t1 i=2 n=2
print 打印: 4:j i=3 n=3
print 打印: 5:構造塊 i=4 n=4
StaticTest 構造方法:  6:t2 i=5 n=5
print 打印: 7:i i=6 n=6
print 打印: 8:靜態塊 i=7 n=99
print 打印: 9:j i=8 n=100
print 打印: 10:構造塊 i=9 n=101
StaticTest 構造方法:  11:init i=10 n=102

在類加載的準備階段是正式爲類變量(靜態變量)分配內存並設置初始值的階段,這些變量所使用的內存將在方法區中進行分配。注意:

  1. 這個時候進行內存分配的僅包括類變量(被static 修飾的變量),而不包括實例變量,實例變量會在對象實例化時隨着對象一起分配在Java堆中。
  2. 這裏所說的初始值“通常情況”下是數據類型的零值。

這裏寫圖片描述

“通常情況” 下初始值是零值,那特殊情況是指:如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value 就會被初始化爲ConstantValue屬性所指的值,如
public static final int value = 123;
編譯時javac 將會爲value 生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置講value賦值爲123。

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