java類加載過程 原

java類加載過程

本篇文章主要是摘抄周志明的《深入理解java虛擬機》這本書

類加載然後被使用最後被卸載,整個聲明週期包括如下: 加載,連接,初始化,使用,卸載。且連接中又包括 驗證,準備,解析這個過程。

加載

"加載"是類加載過程中的一個階段,在加載階段,虛擬機需要完成以下三個事情

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

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

驗證

  1. 文件格式校驗

    • 是否以魔數0xCAFEBABE開頭
    • 主次版本號是否在當前虛擬機處理的範圍
    • 常量池中是否有不背支持的常量類型
    • 只想常量的各種索引值中是否有指向不存在的常量或者不符合類型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的格式
    • Class文件中各個部分及文件本身是否有被刪除或者附加的其他信息
    • ……

    第一輪的驗證遠非這些,上面驗證只是HotSpot中的驗證的一小部分,這部分主要是基於字節流進行驗證的,保證字節流能正確的解析並存儲在方法區之內 ,格式上符合描述一個java類型信息的要求。只有通過這個階段的驗證,字節流纔會進入內存的方法區中進行存儲,所以後面的三個階段全部是基於方法區 的存儲結構進行的,不會再直接操作字節流。

  2. 元數據校驗
    這一階段是對字節碼描述的信息語義分析,保證其描述的信息符合java語言規範的要求,可能包括的驗證點如下:

    • 是否有父類
    • 是否繼承了不允許繼承的類(final修飾)
    • 不是抽象類的話是否實現了接口或者抽象類中的方法
    • 類的字段,方法是否與父類產生衝突,例如複寫了父類的final字段以及不合規的方法重載。
  3. 字節碼驗證
    這個階段是更爲複雜和嚴格的驗證,通過對數據流和控制流的分析,確定語義是合法的,符合邏輯的。這一階段對類的方法體進行校驗分析,保證被校驗類的方法 在運行時不會產生對虛擬機有害的操作,例如:

    • 保證跳轉指令不會跳轉到方法體以外的字節碼指令上。
    • 保證任意時刻操作數棧的數據類型與指令代碼的序列都能配合工作。
    • .....
  4. 符號引用驗證 最後一階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三個階段,解析階段中發生。符號引用驗證的目的是確保解析動作能 正常的執行,如果無法通過符號引用驗證,將會拋出java.lang.NoSuchMethodError,java.lang.NoSuchFieldError,java.lang.IllegalAccessError等 通常會校驗以下內容:

    • 符號引用中通過字符串描述的全限定名是否能找到對應的類
    • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
    • 符號引用中的類,字段,方法的可訪問性(private,public,protected,default)是否可被當前類訪問
    • .......

對於虛擬機類加載機制來說,驗證階段是一個非常重要的階段,但是不是一個必須的階段,以爲對程序運行期沒有影響。如果所運行的代碼是非常可信的,已經被反覆的校驗過,可以考慮使用 -Xverify:none 參數來關閉大部分的類檢驗過程,縮短虛擬機的編譯時間

準備

準備階段是正式爲類變量分配內存地址並設置類變量初始值的階段,這些變量所使用的初始值都將在內存中分配。但是需要注意的是,這時候進行內存分配的僅包括類變量(被static修飾的變量), 並不包括實例變量,實例變量將會在對象實例化的時候隨着對象一起分配在Java堆中。其次,這裏所說的初始值通常情況下是數據類型的零值,假設有如下定義:

public static int value=123;

那變量value在準備階段過後的初始值是0,而不是123,因爲這時候尚未開始執行任何java方法,而把value賦值爲123的putstatic指令是在程序編譯之後,存放於類構造器<clinit>()方法之中,所以 把value賦值爲123的動作將在初始化階段纔會執行。
但是上面所說的通常情況會把value值設置爲0值,但是如果按照下面定義:

public static final int value=123

這時候再準備階段value值就會被初始化爲Constant-Value屬性所值的值,value就會被賦值爲123

解析

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

  • 符號引用:
    符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時無歧義的定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種 虛擬機實現的內存佈局可以各不相同,但是他們能接收的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在了java虛擬機規範的Class文件格式中。
  • 直接引用
    直接引用可以是直接指向目標的指針,相對偏移量或者是一個能簡介定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用, 那引用的目標必定已經在內存中存在。

初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶引用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段才真正開始執行類中定義的java代碼

在準備階段,變量已經賦值過一次初始值,而在初始化階段,則會根據程序初始化類變量和他資源。或者說,初始化階段是執行類構造器<clinit>()方法的過程

<clinit>()方法石油編譯器自動收集類中的所有變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,
編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句之前的變量,
定義在它之後的變量,在前面靜態語句塊可以賦值,但是不能訪問。  
    public static Test{
        static(
            i = 0;//給變量賦值可以正常的編譯通過
            System.out.print(i); //這句編譯器會提示“非法向前引用”
        )
        static int i=1;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章