JVM學習筆記(三)

上回我們通過一個 Hello World 程序,分析了class文件結構,這回我們來分析下JVM是如何加載class文件中的類。

我們在運行java程序之前,肯定需要將程序用到的類加載到虛擬機中,那麼虛擬機是多會加載的類呢。首先虛擬機中規定了java類的整個生命週期如下(順時針):

在這裏插入圖片描述
在JVM規範中並沒有規定何時開始一個類的加載,但是規定了五種情況發生時需要對類立即進行初始化操作,由於初始化是在加載操作之後,所以這五種情況發生時肯定會進行類的加載操作。

下面我們通過幾個簡單的例子來總結是哪五種情況。我們知道一個類在初始化時會先執行static塊和static域變量中的內容,所以下面的代碼通過執行static塊來驗證類初始化情況。

public class StaticGrandfather {

	static {
		System.out.println("grandfather init");
	}
}
public class StaticFather extends StaticGrandfather {

	static {
		System.out.println("father init");
	}
	public static int FATHER_VALUE = 1;
}

public class StaticSon extends StaticFather {

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

	public static int SON_VALUE;

	public static final int SON_FINAL_VAL = 0;
}

public class StaticMethod extends StaticFather {

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

	public static void invokeStaticMethod() {
		System.out.println("run static method");
	}

}

public class MethodHandleTest {

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

	public static void println(String s) {

		System.out.println(s);
	}
}

下面運行如下代碼

public class Main {
	static {
		System.out.println("main init");
	}
	public static void main(String[] args) {
	// 0
		StaticGrandfather sg = new StaticGrandfather();
//		try {
//			StaticGrandfather sg = (StaticGrandfather) Class
//					.forName("com.togo.jvm.load.StaticGrandfather").newInstance();
//		} catch (InstantiationException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		} catch (IllegalAccessException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		} catch (ClassNotFoundException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		}
		
		// 1
//		System.out.println(StaticSon.FATHER_VALUE);
		// 2
//		StaticSon.SON_VALUE = 123;
		// 3
//		System.out.println(StaticSon.SON_FINAL_VAL);
		// 4
//		StaticMethod.invokeStaticMethod();
		//5
//		MethodType mt = MethodType.methodType(void.class, String.class);
//		try {
//			MethodHandle mh = MethodHandles.lookup().findStatic(MethodHandleTest.class, "println", mt);
//			mh.invoke("ss");
//		} catch (NoSuchMethodException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		} catch (IllegalAccessException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		} catch (Throwable e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace();
//		}
	}
}

當我們運行0時(其他代碼註釋,下同),控制檯打印結果爲

main init
grandfather init

0代碼是new了一個對象,通過打印結果可以知道①JVM會先初始化main()方法所在的類;②new對象會觸發該類的初始化。
不光是new操作,通過反射創建對象也是會觸發該類的初始化,這裏就不演示了。

當我們運行1時,控制檯打印結果爲

main init
grandfather init
father init
1

1代碼是通過子類調用了父類的靜態域變量,通過打印結果可以看到父類和祖父類都已經初始化了,但是子類沒有初始化。這裏說明①子類調用父類的靜態域變量並不會讓子類得到加載②而是會初始化該變量所屬的類,③如果初始化的類的父類還沒有初始化,則會先初始化父類。

現在運行2代碼,註釋掉1(下同),運行結果如下

main init
grandfather init
father init
son init

2代碼是給子類的一個靜態域變量賦值,通過打印結果可以看到子類已經初始化了,同樣在初始化子類之前肯定需要初始化父類。所以①給類的靜態變量賦值也會初始化該類。

現在運行與2代碼類似的3代碼,結果如下:

main init
0

從結果看,只是打印了這個變量的值,並沒有初始化這幾個類中的任意一個,這是因爲JVM在編譯階段,已經將static final 修飾的變量的值存儲在了調用類的常量池中,所以代碼中訪問這個值已經跟StaticSon類沒有關係了,所以並不會初始化這個類,即①訪問一個類的常量變量並不會觸發這個類的初始化。

運行代碼4,結果如下

main init
grandfather init
father init
method init
run static method

通過結果得知①訪問類的靜態方法會觸發該類的初始化

以上是一些會觸發初始化的常見操作,下面看看動態語言支持
運行代碼5,結果如下

main init
methodhandle init
ss

《深入理解JAVA虛擬機》原文是:當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行過初始化則需要先觸發初始化。

動態語言實現的效果有點類似反射,但是實現的原理方式不同,最終的目標也不同,後面再詳細介紹。

以上就是會觸發初始化的五種情況,總結一下:
①使用new創建對象;讀取或者設置類的靜態變量(非final字段);調用一個靜態方法。
②使用反射創建一個對象;
③初始化一個類的時候,如果這個類的父類還沒有初始化,則先對父類進行初始化;
④JVM會對main()方法所在的類進行初始化;
⑤當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行過初始化則需要先觸發初始化。

JVM規範中規定“有且只有”這五種情況會觸發類的初始化。

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