類加載過程概括
說說引用
詳解類加載全過程:
加載
驗證
准備
解析
初始化
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是Java虛擬機的類加載機制。
在Java中,類型的加載、連接和初始化過程都在程序運行期間完成的,這種策略雖然會使類加載時增加一些性能開銷,但是提供了高度的靈活性,Java里天生可以動態擴展的語言特就是依賴於運行期動態加載和動態連接的特點實現的。
Class文件指的是一串二進制的字節流。實際上,每個Class文件都有可能代表着Java語言中的一個類或者接口。
類加載的過程
類加載過程概括
在這七個過程中,加載、驗證、准備、初始化、卸載這5個階段的順序是一定的,類的加載過程必須按照這種順序按部就班地開始,而解析過程則不一定:它在某個情況下可以在初始化階段之后再開始,這是為了支持Java語言語言的運行時綁定(也叫動態綁定和晚期綁定)。
這里強調的是:類加載階段都是互相交叉地混合式進行的,通常是在一個階段執行的過程中調用、激活另一階段。
說說引用
對類的初始化操作可分為主動引用和被動引用
主動引用:在以下5種情況下會進行類的主動引用的初始化操作:
- 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令最常見的代碼情景是:使用new關鍵字實例化對象、讀取過設置一個類的靜態字段(被final修飾、已在編譯期把結果放進常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包對類進行反射調用時,如果類沒有進行過初始化,則應需要先觸發其初始化。
- 當初始化一個類時,如果發現其父類還沒有進行過初始化,會觸發其父類實例化。
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main方法類),虛擬機會先實例化那個類。
- 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例的最后解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則要先觸發其初始化。
被動引用:所有引用類的方式都不會觸發初始化。
對於靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
通過數組定義引用類,不會觸發此類初始化:當初始化對象數組時,並不會實際觸發對象的初始化操作。但是會觸發一個是由虛擬機自動生成的、直接繼承於java.lang.Object的子類,創建動作由字節碼指令newarray觸發。值得注意的是:該類代表了實際的對象數組,數組中應有的方法和屬性都實現在這個類里。Java語言對數組的訪問比C/C++相對安全是因為這個類分裝了數組元素的訪問方法。
常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義類的初始化。
值得注意的是:
接口也有自己的初始化過程:編譯器會為接口生成“()”類構造器,用於初始化接口中所定義的成員變量。
接口和類初始化的區別:當一個類在初始化時,其父類都基本上初始化過了,然而接口在初始化的時候,只有真正用到父接口的時候(如引用接口中定義的常量)才會進行初始化。
詳解類加載全過程:
加載
在加載階段,虛擬機需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節流。
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問接口。
非數組類的加載是可控性最強的。用戶除了使用系統提供的引導類加載類來完成,也可以由用戶自定義的類加載器去加載(重寫一個類加載器的loadClass())。
注意:數組類本身不通過類加載器創建,它是由JVM直接創建的。但數組類和類加載器仍有很緊密的關系,因為數組類的元素類型最終是靠類加載器去創建。
加載完成后,虛擬機外部的二進制字節流就按照虛擬機所需格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。然后在內存中實例化一個java.lang.Class類的對象(可以在Java堆中,也可以在方法區中),該對象將作為程序去訪問方法區中的這些類型數據的外部接口。
加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未結束,連接階段就可能開始了。但是夾在加載階段進行的動作,仍然屬於連接階段的內容。
驗證
驗證是連接的第一步,目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危及虛擬機本身的安全。
驗證階段的四個步驟:文件格式檢驗、元數據檢驗、字節碼檢驗、符號引用檢驗。
- 文件格式檢驗:
檢驗字節流是否符合Class文件格式的規范,並且能被當前版本的虛擬機處理。檢驗可能包含下列幾種:是否以魔數開頭、主次版本號是否在虛擬機的處理范圍之內,常量池中的常量是否不被支持、文件是否被刪除或附加什么信息等等。
只有通過文件格式檢驗的二進制字節流才能進入內存的方法區進行存儲,所以后面的3個檢驗階段都是基於方法區的存儲結構進行的,不會在操作字節流。 - 元數據檢驗:
對字節碼描述的信息進行語義分析,以保證其描述的內容符合Java語言規范的要求。
驗證點包括:是否有父類(除了object)、父類是否繼承了不可被繼承的類(被final修飾的類)、如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法、類中的方法和字段是否與父類產生矛盾(覆蓋了父類的final字段、出現不合規矩的方法重載等)。
元數據檢驗主要是對類的元數據信息進行語義校驗,保證不符合Java語言規范的元數據信息不存在。 - 字節碼檢驗:
通過數據流和控制流分析,確定程序語義是合法、符合邏輯的。第二階段是對元數據信息中的數據類型做了檢驗,這一階段是對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事情。
檢驗點包括:保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作、保證指令跳轉不會跳轉到方法體之外的地方、保證方法體內的類型轉換都是有效的。
事實上,即便是經過字節碼檢驗后的方法體也不一定是安全的。 - 符號引用檢驗
最后一個檢驗發生在虛擬機將符號引用轉化為直接引用時,這個轉化動作將在連接的第三階段–解析階段中發生的。符號引用檢驗可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。
校驗點:符號引用中通過字符串描述的全限定名是否能找到對應的類、在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段、符號引用中的類、字段、方法的訪問權限是否能讓當前類訪問到等。
符號引用檢驗的目的是確保解析動作的正常執行,如果無法通過符號引用檢驗,將會拋出java.lang.IncompatibleClassChangeError異常的子類,如IllegalAccessError、NoSuchfiledError、NoSuchMethodError等。
准備
准備階段是正式為類變量分配內存並設置類變量初始值的階段。這些變量所使用的內存將在方法區中進行分配。此時進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。另外,在這里分配的靜態類變量是將其值定義為0等默認值,而不是我們定義的。因為這時尚未執行任何Java方法,我們定義的賦值的putStatic指令是程序被編譯后,存放在類構造器()方法中,所以正確的賦值將在初始化階段執行。
如果類變量被final修飾,那么在這種情況下,在編譯時Javac將會為該變量生成ConstantValue屬性,在准備階段虛擬機會根據該屬性設置類變量的正確值。
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
a、符號引用:以一組符號來描述所引用的目標,符號可以是任何形式字面量,只要使用時無歧義地定位到目標就行。
b、 直接引用:直接引用是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。引用的目標已經在內存中存在。
虛擬機實現可以根據需要來判斷到底在類被加載器加載時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用時才去解析它。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
初始化
類加載的最后一步,真正執行類中定義的Java程序代碼(字節碼)。
初始化階段是執行類構造器()方法的過程,根據程序員通過程序制定的主觀的計划去初始化類變量和其他資源。