有一道非常經典的題目,如果對虛擬機加載類的過程不熟悉,很容易就答錯,題目如下:
public class Singleton
{
public static Singleton instance = new Singleton();
public static int a;
public static int b = 0;
private Singleton()
{
a++;
b++;
}
public static Singleton getInstance()
{
return instance;
}
public static void main(String[] args)
{
Singleton s = Singleton.getInstance();
System.out.println(s.a);
System.out.println(s.b);
}
}
問上面這段代碼輸出的內容是什麼?答案是1,0。如果您能答上來說明您對類加載的過程已經很熟悉了,下面就來分析一下爲什麼會有上面的結果。
首先了解一個概念,主動引用,jvm規範中規定有且只有下面幾種纔是主動引用,主動引用會觸發類的初始化。
1.遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
上面的四條指令都是字節碼指令,可以理解爲new,獲取靜態屬性,設置靜態屬性,調用靜態方法。
a. new 一個類的時候會發生初始化
b.調用類中的靜態成員,除了final字段,看下面這個例子,final被調用但是沒有初始化類
這裏注意是除了final字段,因爲final字段在編譯期已經將值存儲到了類的常量池中,因此引用final的靜態成員是,不會導致初始化動作。
c. 調用某個類中的靜態方法,那個類一定先被初始化了
2.使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3.當初始化一個類的時候,如果發現其父類還沒進行過初始化,則需要先觸發其父類的初始化。
4.當虛擬機啓動時,用戶需要指定一個要執行的主類,虛擬機會先初始化這個主類。
除了上面四種情況的主動引用,還要注意有三種被動引用並不會觸發類的初始化
1.通過子類引用父類的靜態字段,不會導致子類初始化
2.通過數組定義類引用類,不會觸發此類的初始化
3.常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
因此Singleton在jvm找到main方法入口的時候,便會進行類的初始化動作。
類的初始化包括下面幾個步驟:
1.類的加載,由classloader講二進制文件加載到內存。
2.連接階段,其中該階段又分爲三個過程
a.驗證,驗證加載進來的字節碼的合法性。
b.準備,爲類的靜態變量分配內存並初始化爲默認值(int爲0,double爲0.0等等這些,並不是指代碼中=後面的值,注意此時類的實例還沒有生成,因此不涉及實例變量)
c.解析,將符號引用解析爲直接引用。
3.初始化,將類的靜態變量初始化爲程序中的值。
對於Singleton,在連接階段的第二步,instance會被賦值爲null,a和b會被賦值爲0。然後此時進行第三步初始化,在初始化instance的時候也即new Singleton,會執行構造函數,此時a變爲1,b變爲1,然後再去初始化a,由於沒有賦值動作,故a仍然爲1,但是在初始化b的時候,b會被重新賦值爲0,因此在打印的時候b輸出的爲0。
因爲對於static的初始化是按照定義的順序進行的,因此如果將public static Singleton instance = new Singleton();放到最後初始化,則打印的a和b都爲1。
爲了更好的理解上面的過程,通過javap命令將class文件的虛擬指令輸出,命令如下javap -verbose -private Singleton > Singleton.txt,執行該命令前請先試用javac編譯Singleton。內容如下:
Compiled from "Singleton.java"
public class Singleton extends java.lang.Object
SourceFile: "Singleton.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #10.#27; // java/lang/Object."<init>":()V
const #2 = Field #8.#28; // Singleton.a:I
const #3 = Field #8.#29; // Singleton.b:I
const #4 = Field #8.#30; // Singleton.instance:LSingleton;
const #5 = Method #8.#31; // Singleton.getInstance:()LSingleton;
const #6 = Field #32.#33; // java/lang/System.out:Ljava/io/PrintStream;
const #7 = Method #34.#35; // java/io/PrintStream.println:(I)V
const #8 = class #36; // Singleton
const #9 = Method #8.#27; // Singleton."<init>":()V
const #10 = class #37; // java/lang/Object
const #11 = Asciz instance;
const #12 = Asciz LSingleton;;
const #13 = Asciz a;
const #14 = Asciz I;
const #15 = Asciz b;
const #16 = Asciz <init>;
const #17 = Asciz ()V;
const #18 = Asciz Code;
const #19 = Asciz LineNumberTable;
const #20 = Asciz getInstance;
const #21 = Asciz ()LSingleton;;
const #22 = Asciz main;
const #23 = Asciz ([Ljava/lang/String;)V;
const #24 = Asciz <clinit>;
const #25 = Asciz SourceFile;
const #26 = Asciz Singleton.java;
const #27 = NameAndType #16:#17;// "<init>":()V
const #28 = NameAndType #13:#14;// a:I
const #29 = NameAndType #15:#14;// b:I
const #30 = NameAndType #11:#12;// instance:LSingleton;
const #31 = NameAndType #20:#21;// getInstance:()LSingleton;
const #32 = class #38; // java/lang/System
const #33 = NameAndType #39:#40;// out:Ljava/io/PrintStream;
const #34 = class #41; // java/io/PrintStream
const #35 = NameAndType #42:#43;// println:(I)V
const #36 = Asciz Singleton;
const #37 = Asciz java/lang/Object;
const #38 = Asciz java/lang/System;
const #39 = Asciz out;
const #40 = Asciz Ljava/io/PrintStream;;
const #41 = Asciz java/io/PrintStream;
const #42 = Asciz println;
const #43 = Asciz (I)V;
{
public static Singleton instance;
public static int a;
public static int b;
private Singleton();
Code:
Stack=2, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: getstatic #2; //Field a:I
7: iconst_1
8: iadd
9: putstatic #2; //Field a:I
12: getstatic #3; //Field b:I
15: iconst_1
16: iadd
17: putstatic #3; //Field b:I
20: return
LineNumberTable:
line 10: 0
line 11: 4
line 12: 12
line 13: 20
public static Singleton getInstance();
Code:
Stack=1, Locals=0, Args_size=0
0: getstatic #4; //Field instance:LSingleton;
3: areturn
LineNumberTable:
line 16: 0
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=2, Args_size=1
0: invokestatic #5; //Method getInstance:()LSingleton;
3: astore_1
4: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_1
8: pop
9: getstatic #2; //Field a:I
12: invokevirtual #7; //Method java/io/PrintStream.println:(I)V
15: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_1
19: pop
20: getstatic #3; //Field b:I
23: invokevirtual #7; //Method java/io/PrintStream.println:(I)V
26: return
LineNumberTable:
line 21: 0
line 22: 4
line 23: 15
line 24: 26
static {};
Code:
Stack=2, Locals=0, Args_size=0
0: new #8; //class Singleton
3: dup
4: invokespecial #9; //Method "<init>":()V
7: putstatic #4; //Field instance:LSingleton;
10: iconst_0
11: putstatic #3; //Field b:I
14: return
LineNumberTable:
line 4: 0
line 7: 10
}
注意最下面的static{}靜態塊,所有的靜態屬性都會在該塊中被初始化,該初始化對應的就是第三步。
首先new執行,在堆中生成Singleton的實例,並將指向該實例的指針壓入操作數棧(棧幀的組成元素之一,還有一個要了解的是一組局部變量,下標從0開始)中。
dup命令複製操作數棧棧頂的值,注意此時操作數棧有兩項值且都爲this引用
接下來的invokespecial通過棧頂的this引用調用構造方法,消耗棧頂的this引用。
轉到構造函數中,注意有一行Stack=2, Locals=1, Args_size=1所有的方法都有一行類似的數據,其中Stack=2表示操作數棧的長度爲2個slot,其中一個slot佔用四個字節,Locals=1表示本地變量表長度爲1,因爲這裏用到了this指針,默認方法的本地變量第一個值爲this引用,Args_size=1這裏表示傳入方法的參數,所有的實例方法至少都會是1,因爲默認會傳入this指針,因此這裏的構造函數默認會接收this參數。
aload_0表示將局部變量數組中索引爲0的值壓棧,這裏將this壓棧,然後invokespecial調用Object的初始化方法,getstatic方法獲取a的值並壓棧,這裏a爲0,然後iconst_1將1壓棧,此時棧裏有兩個值,分別爲0和1,iadd彈出棧頂的兩個值然後相加並將結果壓棧。putstatic將結果1賦值給a,後面類似的操作將結果1賦值給b。初始化方法返回,繼續前一個棧幀的執行即static{}塊。
putstatic將this引用賦值給instance變量,此時操作數棧爲空
iconst_0,將0入棧
putstatic將0賦值給變量b,此時b又變回了0,因此b最終的結果爲0。
弄清楚整個問題的關鍵是掌握jvm對於類變量的初始化過程。首先是爲類變量分配內存並初始化爲默認值,此爲一個階段,然後是代碼本身在構造函數中的初始化並不代表最終的值,因爲jvm還會對類變量進行初始化動作,即執行等號動作b=0;該動作會被編譯到static{}塊中,static塊中初始化的順序和代碼中申明的順序有關,也就是構造函數被調用的順序影響到最終的值,而構造函數被觸發的條件就是new動作的執行。因此最後的輸出和public static Singleton instance = new Singleton();的順序有關。