面試官,不要再問我「Java虛擬機類加載機制」了

關於Java虛擬機類加載機制每每有兩方面的面試題:根據程序判斷輸出結果和講講虛擬機類加載機制的流程。其實這兩類題本質上都是考察面試者對Java虛擬機類加載機制的瞭解。java

面試題試水

如今有這樣一道判斷程序輸出結果的面試題,先看看打印的結果是什麼?面試

public class SuperClass {

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

	public static String ABC = "abc";
}

public class SubClass extends SuperClass{

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

public class Main {

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

}

複製代碼

上面定義了三個類,其中SubClass繼承SuperClass,而後Mian類中打印SubClass.ABC的值。那麼,控制檯打印結果是什麼?數據庫

SuperClass static init
abc
複製代碼

你作對了麼?這是爲何呢?對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。數組

再對上面的代碼進行調整,對靜態變量ABC添加final修飾。安全

public class SuperClass {

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

	public static final String ABC = "abc";
}

public class SubClass extends SuperClass{

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

public class Main {

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

}
複製代碼

打印結果爲:bash

abc
複製代碼

這又是爲何呢?由於,常量在編譯階段會存入調用類的常量池中,也就是說Main類對SubClass.ABC的引用已經與SuperClass無關了,實際上已經轉行爲Main類對ABC的引用了。微信

作好的鋪墊,能夠開始對類加載機制的瞭解了。網絡

類加載過程

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉化解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。數據結構

整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)。多線程

Java虛擬機類加載機制

其中加載、驗證、準備、初始化和卸載的執行順序是肯定的,解析階段則在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。

加載階段

在加載階段虛擬機會完成三件事:

  • 經過一個類的全限定名來獲取定義此類的二進制字節流;
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;

其中獲取二進制字節流能夠經過Class文件、ZIP包、網絡、運行時(動態代理)、JSP生成、數據庫等途徑獲取。

須要注意的是數組類的加載,數組類並不經過類加載器加載,而是由Java虛擬機直接建立,但數組類的元素仍是要依靠類加載器進行加載。

這些二進制字節流加載完成以後,按照指定的格式存放于于方法區內(Java7及之前方法區位於永久代,Java8位於Metaspace)。而後在方法區生成一個比較特殊的java.lang.Class對象,用來做爲程序訪問方法區中這些類型數據的外部接口。

驗證階段

驗證的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

Java虛擬機類加載機制

文件格式驗證:驗證字節流是否符合Class文件格式的規範;好比,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。只有驗證經過纔會進入方法區進行存儲。

元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求;好比,是否有父類(除Object類)、父類是否爲final修飾、是否實現抽象方法或接口、重載是否正確等。

字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。好比,保證數據類型與指令正常配合工做、指令不會跳轉到方法體外的字節碼上,方法體中的類型轉換是有效的等。

符號引用驗證:在虛擬機將符號引用轉化爲直接引用的時候進行驗證,能夠看作是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。常見的異常好比:java.lang.NoSuchMethdError、java.lang.NoSuchFiledError等。

準備階段

準備階段主要是正式爲類變量分配內存並設置類變量初始值,變量所使用的內存都將在方法區中進行分配。

此處的類變量指的是被static修飾的變量,不包含實例變量,實例變量在對象實例化階段分配在堆中。

public static String ABC = "abc";
複製代碼

而且,變量的初始化值並非類中定義的值,而是該變量所屬類型的默認值。

Java虛擬機類加載機制

固然,也有特殊狀況,好比當變量被final修飾時:

public static final String ABC = "abc";
複製代碼

此時,該字段屬性是ConstantValue時,會在準備階段初始化爲指定的值。

解析階段

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

這裏咱們看一下字段解析,也就是最開始第一道面試題。當獲取SubClass的屬性ABC時,首先會查找SubClass自己是否包含該字段,若是包含則直接返回引用,查找結束。

不然,若是SubClass類實現了接口或繼承了父類,那麼則遞歸搜索各個接口和父類,找到匹配的屬性則返回,查找結束。

不然,查找失敗,拋出java.lang.NoSuchFieldError異常。若是返回成功了,可是是權限校驗失敗,也就是無該字段的訪問權限,則拋出java.lang.IllegalAccessError異常。

其餘形式的解析,就再也不這裏一一說明了。

初始化階段

初始化階段纔是真正執行類中定義的Java程序代碼(字節碼)。在此階段會根據代碼進行類變量和其餘資源的初始化,或者能夠從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static語句塊)中的語句合併生成的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊中能夠賦值,可是不能訪問。

編譯器提示錯誤。

Java虛擬機類加載機制

將其放在後面,則正常編譯執行,輸出結果爲「edf」:

Java虛擬機類加載機制

若是將static中的打印語句去掉,那麼下面這段代碼的打印結果會是什麼呢?

public class Main {
	static {
		//能夠賦值
		abc = "edf";
		//編譯器會提示「非法向前引用」
//		System.out.println(abc);
	}

	static String abc = "abc";

	public static void main(String[] args) {
		System.out.println(abc);
	}
}
複製代碼

打印結果爲「abc」。在準備階段屬性abc的值爲null,而後類初始化按照順序執行,首先執行static塊中的abc=「edf」賦值操做,接着執行abc="abc"的賦值操做,此時值爲「abc」。當main方法調用打印時則爲「abc」。

<clinit>()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<cinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。最開始的面試題中打印出父類靜態塊的方法就是這個緣由。

因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。

<clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產<clinit>()方法。

接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的。

虛擬機規範初始化

虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

  • 遇到new,getstatic,putstatic,invokestatic這失調字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
  • 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先出觸發其初始化。

該段內容引自周志明《深刻理解java虛擬機》。

小結

通過以上步驟,便完成了虛擬機類的加載過程,後續會繼續講解虛擬機的類加載器和雙親委派機制。歡迎你們關注公衆號「程序新視界」繼續深刻學習。

原文連接:《面試官,不要再問我「Java虛擬機類加載機制」了

《面試官》系列文章:


程序新視界:精彩和成長都不容錯過

程序新視界-微信公衆號
相關文章
相關標籤/搜索