深入理解 Java 虛擬機:類的加載過程

類的生命週期

類從被加載到虛擬機內存中,到卸載出內存爲止,整個生命週期包含:加載驗證準備解析初始化使用卸載

其中 類的加載過程 包含:加載驗證準備解析初始化

驗證準備解析 統稱爲 連接

加載

類加載的三步驟:

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

非數組類的加載:
一個 非數組類加載過程(準確的說是加載階段中獲取二進制流的動作) 是開發人員可控性最強的,加載階段 可使用系統提供的 引導類加載器來完成,也可以由 用戶自定義加載器 去完成,開發人員可以通過 自定義類加載器 去控制 字節流 的獲取方式(即重寫一個類加載器的 loadClass() 方法)。

數組類的加載:
數組類 的本身並不通過 類加載器 創建,它由 Java 虛擬機直接創建。但數組類的 元素類型,最終要靠 類加載器 去創建。

驗證

目的: 這一階段是爲了確保 Class 文件 的字節流中包含的信息 符合當前虛擬機的要求,並且不會危害虛擬機自身的安全

文件格式驗證

保證輸入的字節流能 正確地解析並存儲於方法區之內,格式上符合 Java 的信息要求,成功後字節流會被讀入 方法區 中進行存儲,所以後續的 3 個驗證階段都是基於方法區中的存儲結構,不再操作字節流。

驗證的內容:

  1. 是否以魔數 0xCAFEBABE 開頭
    0xCAFEBABE(咖啡寶貝?) 是 Java 編譯後 Class 文件的開頭標識,有這個說明是 Java 的 Class 文件
  2. 主、次版本號是否在當前虛擬機的處理範圍內
    不少人應該都遇到過,由於 JDK 版本太低,導致的 major.minor version 52.0,百度下發現要用 JDK8
  3. 常量池中的常量是否有不被支持的類型(檢查常量 tag 標誌)
  4. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 編碼的數據
  5. Class 文件 中各個部分及文件本身是否有被刪除的或附加的其他信息

元數據驗證

對類的元數據進行驗證,保證 不存在不符合 Java 語言規範的元數據信息

驗證內容:

  1. 這個類是否有父類(除了 Object 類都應該有父類)
  2. 這個類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)
  3. 如果這個類不是抽象類,是否實現了 父類接口 中要求實現的所有方法。
  4. 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的 final 字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不同等)

字節碼驗證

最複雜的階段,通過數據流和控制流的分析,確認軟件語義是 合法符合邏輯 的,保證被校驗類的方法 在運行時不會做出危害虛擬機安全的事件

例如:

  1. 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作。
  2. 保證指令不會跳到方法體以外的字節碼指令上。
  3. 保證方法體中的類型轉換是有效的。

符號引用驗證

驗證虛擬機將 符號引用 轉化爲 直接引用 時,進行的 匹配性校驗

驗證內容:

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

準備

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

初始值爲零值:
這時候進行內存分配的僅包括 類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在 Java 堆中

其次,這裏所說的初始值 “通常情況” 下是數據類型的 零值,假設一個類變量的定義爲:

public static int value=123;

那變量 value準備階段過後的初始值爲 0 而不是 123
而把 value賦值爲 123putstatic 指令是程序被編譯後,存放於類構造器 <clinit>()方法之中,所以把value 賦值爲 123 的動作將在 初始化階段 纔會執行

特殊情況:
如果類字段的字段屬性表中存在 ConstantValue 屬性,那在準備階段變量 value 就會被初始化爲 ConstantValue 屬性所指定的值,例如:

public static final int value=123;

編譯時 Javac 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 123`。

解析

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

符號引用與直接引用:

  • 符號引用: 以一組符號來描述所引用 目標,符號可以是 任何形式的字面量,只要使用是能 無歧義的定位到目標即可。符號引用與虛擬機內存佈局無關,引用的目標 並不一定加載導內存中符號引用的字面量形式明確定義在 Class 中
  • 直接引用: 直接引用可以是直接指向目標的指針、相對偏移量或者一個能 能間接定位到目標的句柄。如果有 直接引用,那引用的目標 必定已經在內存中存在

解析動作的目標:
類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行

注:解析的過程比較複雜,具體可以參考《深入理解java虛擬機》,個人認爲大致瞭解即可

初始化

準備階段 變量已經經過了初始值(零值)的賦值,而在 初始化階段 將根據 程序員的主觀意願去初始化變量和其他資源。初始化階段是執行 類構造器 <clinit>() 方法 的過程。

類構造器 <clinit>():

  • <clinit>() 方法是由編譯器自動收集類中 所有類變量的賦值動作靜態語句塊 static{} 的語句合併產生的。靜態語句塊中 只能訪問到靜態語句塊之前的變量之後的變量只能賦值不能訪問。
public class Test {
	static {
		// 給變量賦值可以正常通過
		i = 0;
		//編譯報錯,提示 “非法向前引用”
		System.out.println(i);
	}
	static int i = 0;
}
  • <clinit>() 方法與類的構造函數不同,它不需要顯示的調用父類構造函數,虛擬機會保證子類的 <clinit>() 調用之前,父類的 <clinit>() 方法已經執行完畢。因此虛擬機第一個被加載的 <clinit>() 肯定是 java.lang.Object
  • 如果一個類 沒有靜態語句塊,則 不會生成 <clinit>()
  • 接口不能使用靜態語句塊,但是依然有 變量初始化 的賦值操作,因此也會生成 <clinit>()。但接口與類不同,接口不需要先執行父類接口的 <clinit>() 方法。只有當 接口中的變量使用時,父接口才會喫實話,另外,接口實現類在初始化時也一樣不會執行接口的 <clinit>() 方法。
  • 虛擬機會保證一個類的 <clinit>() 方法在多線程環境下是正確的加鎖、同步,多個線程去初始化一個類是,只會有一個類執行 <clinit>(),其他的阻塞等待

總結

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章