1. 程式人生 > >JVM學習記錄-類加載的過程

JVM學習記錄-類加載的過程

實例 出發 修飾 調用父類 數據驗證 自己的 one 加載 句柄

類的整個生命周期的7個階段是:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)。

類加載的全過程主要包括:加載、驗證、準備、解析、初始化這5個階段的內容。

加載

加載是類加載過程的一個階段, 在加載階段JVM需要完成以下3件事情:

  1. 通過一個類的全限定明來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化為方法區運行時數據結構。
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據訪問入口。

加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是整個類加載過程中開發人員可控性最強的,因為加載階段既可以使用系統提供的引導類加載器完成,又可以由用戶自定義的二類加載器去完成,開發人員可以通過定義自己的類加載器區控制字節流的獲取方式。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,方法區中的數據存儲格式由虛擬機的實現自行定義,虛擬機規範未規定此區域的具體數據結構。然後再內存中實例化一個java.lang.Class類的對象(這個對象,並沒有要求必須是在Java堆中,就HotSpot而言,Class對象比較特殊,雖然是對象,但是是存放在方法區中的),這個對象將作為程序訪問方法區中的這些類型數據的外部接口。

加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證東西)是交叉進行的,但是這兩個階段的開始時間仍然保持著固定的 先後順序。

驗證

驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊,它大致上會完成4個階段的檢驗工作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

文件格式驗證

這一階段主要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。

驗證內容包括:是否以魔數0xCAFEBABE開頭,主次版本號是否在當前虛擬機處理範圍之內,常量池的常量是否有不被支持的常量類型,指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量,CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據,Class文件中各個部分及文件本身是否有被刪除的或附近的其他信息等等。

元數據驗證

第二階段主要是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息。

驗證內容包括:當前類是否有父類(除了Object類之外,所有類都該有父類),當前類的父類是否繼承了不被允許繼承的類(被final修飾的類),如果當前類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法,類中的字段、方法是否與父類產生矛盾(如覆蓋了父類的final字段等)等等。

字節碼驗證

第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

驗證內容包括:保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如:保證不會出現在操作棧放置了一個int類型的數據,使用時卻按long類型來加載如本地變量表中。保證跳轉指令不會跳轉到方法體以為的字節碼指令上。保證方法體上的類型轉換是有效的,例如:可以把一個子類對象賦值給父類數據類型,但是不能把父類對象賦值給子類數據類型。

符號引用驗證

最後一個階段的校驗發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作發生在解析階段。符號引用驗證可以看做是對類自身以外的信息進行匹配校驗。

驗證內容包括:符號引用通過字符串描述的全限定明是否能找到對應的類。在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。符號引用中的類、字段、方法的訪問性是否可以被當前類訪問等等。

準備

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段分配內存的僅僅是類變量不包括實例變量。實例變量實在對象實例化的時候分配在堆內存中的,還有就是這裏給類變量設置的初始值“通常情況下”下是數據類型的零值,例如:

public static int value = 666;

這個變量value的值在準備階段被設置的初始值為0而不是666,因為此時尚未開始執行任何Java方法,而把value賦值為666的putstatic指令是程序編譯後,存放於類構造器<clinit>()方法之中,所以把value賦值為666的動作將在初始化階段才會執行。

上面說到在“通常情況”下初始值是零值,在非“通常情況”下也就是類字段屬性中存在常量屬性的時候,那麽在準備階段類變量就會被初始化為常量屬性所指定的值。

public static final int value = 666;

編譯時Javac將會生成常量屬性,在準備階段虛擬機就會根據常量屬性的設置將value賦值為666;

解析

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。

符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用目標並不一定已經加載到內存中。

直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有直接引用,那引用的目標必定已經在內存中存在。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符,這7類符號引用,分別對應於常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 這7中常量類型。

初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。在準備階段,變量已經賦過一次系統初始零值了,而在初始化階段,是通過程序制定的主觀計劃去初始化類變量和其他資源,也就是執行類構造器<clinit>()方法的過程。在上一篇“類的加載時機”中已經介紹過了,有5中情況會出發類初始化,下面介紹的是在<clinit>()方法執行過程中一些可能會影響程序運行行為的特點和細節。

  • <clinit>()方法是由編譯器自動收集類中的所有類變量賦值動作和靜態語句塊(static{})中的語句合並產生的,編譯器收集順序室友語句在源文件中出現的豎線所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面靜態語句塊可以賦值,但是不能訪問。
  • <clinit>()方法與類的構造函數不同,它不需要顯示的調用父類構造器,所以虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。
  • 由於父類的<clinit>()方法先執行,也就意味著福利中定義的靜態語句塊要由於子類的變量賦值操作。
  • <clinit>()方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麽編譯器可以不為這個類生產<clinit>()方法。
  • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口和類一樣都會生成<clinit>()方法。接口中只有在使用父接口的時候才會初始化父接口(上一篇已經講解過)。
  • 虛擬機會保證一個類的<clinit>()方法在多線程的環境中被正確地枷鎖、同步,如果多個線程同時去初始化一個類,那麽只會有一個線程區執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程<clinit>()方法

JVM學習記錄-類加載的過程