在這本書里面,在講到類初始化的五種情況時,提及了一個比較有趣的事情。先來看看下面的代碼
public class SubClass {
static{
System.err.println("I m your son");
}
public static final int name = 111;
}
這個時候如果調用SubClass.name,是根本不會觸發SubClass初始化的(這里是因為name是一個常量,和下面的例子不一樣,如果這里把final去掉,是會觸發Subclass的初始化的,因為對于靜態字段而言,如果靜態字段被引用,就會調用getstatic指令和putstatic指令,那么自然就會引發類的初始化,詳情看下面關于觸發類初始化的五種情況)。再來看看另一種情況;
public class SuperClass {
static{
System.err.println("I am your father");
}
public static int value = 123;
}
public class SubClass extends SuperClass{
static{
System.err.println("I m your son");
}
}
這個時候如果調用SubClass.value(靜態字段和靜態方法是可以繼承但是無法被覆蓋,所以這里調用value,只會導致直接定義這個靜態變量的類被初始化),同樣也是不會使得SubClass這個類進行初始化。那么問題來了,到底類在什么時候會進行初始化,類的初始化順序到底是怎樣的?讓我們接著往下看。
一. 類加載的過程
虛擬機加載類主要有五個過程:加載、驗證、準備、解析和初始化。
加載:加載是“類加載”的一個過程,希望讀者沒有混淆這兩個概念。
在這個過程虛擬機主要完成三件事,
? 通過一個類的全限定名___[解釋全限定名]___來獲取此類的二進制字節流,這點上,虛擬機并沒有指明要從哪里獲取類的二進制字節流,因此發展出了很多不一樣的加載方式。比如jar,zip等壓縮包中加載,從網絡獲取[如Applet],或者由其他文件生成[如從JSP生成]。
? 將字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
? 在Java堆[這個沒有強制規定,比如HotSpot則選擇在方法區中生成這個對象]中生成一個代表這個類的java.lang.Class對象,作為程序訪問方法區中的各種數據的外部入口[也就是說當常量池表中的數據被轉換成運行時數據結構的時候,實際上[堆/方法區]有一個Class對象的實例可以訪問到方法區的各類數據,包括常量池表,代碼等]。
如果加載對象是普通的類或者接口(統稱為C),則是通過類加載器(L)去加載C的二進制表示來創建。但是如果加載的是數組類,那情況就有所不同了,數組類本身不通過類加載器創建,它是由Java虛擬機直接創建的。但是數組類內部的元素類型最終還是要靠類加載器去加載。[后續可以添加類加載器的詳細解釋]
驗證
驗證是鏈接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機本身的安全。驗證大致上有以下4個過程:
1) 文件格式驗證:
a) 檢查魔數,主、次版本號是否在當前虛擬機處理范圍。
b) 常量池的常量是否不被支持[通過檢查tag],指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
c) CONSTANT_Utf8_info類型的常量中是否有不符合UTF8編碼的數據。
d) Class文件中各個部分及文件本身是否有被刪除或者附加其他信息等等。
這個節點的主要目的是保證輸入的字節流能被正確的解析并存儲于方法區內,格式上符合描述一個java類型信息的要求。這個階段是基于二進制流,只要通過了這個階段的驗證,字節流才會進入內存的方法區中存儲。所以后續的三個階段基于方法區的存儲結構進行的,不會再直接操作字節流。
2) 元數據驗證:這個階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規范的要求。主要驗證包括以下幾點:
a) 這個類是否有父類(除了java.lang.Object外,所有的類都應該有父類)。
b) 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
c) 如果這個類不是抽象類,那么應該實現其父類或接口中要求實現的方法。
d) 類中的字段,方法是否與父類相矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載)。
這個階段主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java規范的元數據信息。
3) 字節碼驗證:
4) 符號引用驗證:
準備
準備階段是正式為類變量分配內存并設置類變量的初始值階段,這些變量所使用的內存都將在方法區中分配。這里有幾個值得注意的點:
1) 這里初始化的僅僅是類變量(被static修飾的變量)的初始化,并不包括實例變量。實例變量將會在對象實例化的時候隨著對象一起分配在java堆中。
2) 這里所說的初始值,通常是數據類型的零值,舉個例子:
public static int value = 123;
這句代碼中,value在準備階段的初始值為0,而不是123,因為這個時候還沒開始執行任何的java方法。而把value的值置為123的putstatic指令是程序被編譯后,存放在類構造器()方法中的。所以value置為123是在初始化[第五階段]階段才會執行。[還有一些其他類型的零值,可以參考虛擬機規范]
當然,上述情況也有例外的地方,如果類字段的字段屬性表(參考class文件中的屬性數據結構)中存在ConstatntValue[即同時被final和static修飾]屬性,那么在準備階段,變量value就會被初始化為ConstantValue屬性所指定的值,例如上述變量中,編譯時javac將會為value生成的ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue屬性而將value賦值為123。
解析
解析階段就是虛擬機將常量池內的符號引用[使用一組描述符來描述所引用的目標,符可以是任意形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中]替換為直接引用[直接引用可以是直接指向目標的指針,相對偏移量或者一個能間接定位到目標的句柄。直接引用與內存的布局有關,如果有了直接引用,則目標一定存在]的過程,符號引用在Class文件內的常量池中以CONSTANT_Fieldref_info,CONSTANT_Class_info,CONSTANT_Methodref_info等類型出現。那么,解析階段中的直接引用于符號引用又有什么關聯呢?
對同一個符號引用進行多次解析請求是很常見的,比如你在代碼里面多次new同一個類。這里要分成兩種情況:
1) invokeddynamic指令:這個指令的特殊之處在于,它是為了支持動態語言而存在的,也就是說,必須等到程序實際運行這條指令的時候,解析動作才能進行[目前僅使用java語言并不會生成這條指令]。相對的,其余觸發的解析指定都是“靜態”的,可以在剛剛完成加載階段,還沒開始執行代碼時就進行解析。
2) 除了上述的指令外,虛擬機實現可以對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,并把常量標識為已解析狀態)。從而避免了多次解析。
解析動作主要針對“類或接口”,“字段”,“類方法”,“接口方法”,“方法類型”,“方法句柄”和“調用點限定符”7類符號引用進行[分別對應7種常量池表的CONSTATN_Class_info,CONSTATN_Fieldref_info,CONSTATN_Methodref_info,CONSTATN_InterfaceMethodref_info,CONSTATN_MethodType_info,CONSTATN_MethodHandle_info,CONSTATN_InvokeDynamic_info,后續三種和動態類型有關,目前java還是靜態類型語言]。
初始化
在虛擬機中嚴格規定需要對類進行初始化的,有下面五種情況:
1) 遇到new,getstatic,putstatic或者invokestatic這4條字節碼指令時。
2) 使用java.lang.reflect包的方法對類進行反射調用的時候。
3) 當初始化一個類,發現其父類并沒有初始化時,需要先初始化父類。
4) 虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
5) 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有初始化,則需要先觸發其初始化。
對于以上五種初始化場景,虛擬機規范中使用了“只有”,除此之外,所有的引用類的方式都不會觸發初始化。