從一道題目看類加載

有一道非常經典的題目,如果對虛擬機加載類的過程不熟悉,很容易就答錯,題目如下:

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();的順序有關。

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