類的生命週期
類從被加載到虛擬機內存中,到卸載出內存爲止,整個生命週期包含:加載
、驗證
、準備
、解析
、初始化
、使用
、卸載
。
其中 類的加載過程 包含:加載
、驗證
、準備
、解析
、初始化
驗證
、準備
、解析
統稱爲 連接
加載
類加載的三步驟:
- 通過一個類的 全限定名 獲取定義此類的 二進制字節流
- 將字節流所代表的 靜態存儲結構 轉化爲 方法區的運行時數據(永久代的常量池)
- 在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入口
非數組類的加載:
一個 非數組類
的 加載過程(準確的說是加載階段中獲取二進制流的動作) 是開發人員可控性最強的,加載階段 可使用系統提供的 引導類加載器來完成,也可以由 用戶自定義加載器 去完成,開發人員可以通過 自定義類加載器 去控制 字節流
的獲取方式(即重寫一個類加載器的 loadClass()
方法)。
數組類的加載:
數組類
的本身並不通過 類加載器 創建,它由 Java 虛擬機直接創建。但數組類的 元素類型,最終要靠 類加載器 去創建。
驗證
目的: 這一階段是爲了確保 Class 文件 的字節流中包含的信息 符合當前虛擬機的要求
,並且不會危害虛擬機自身的安全
文件格式驗證
保證輸入的字節流能 正確地解析並存儲於方法區之內,格式上符合 Java 的信息要求
,成功後字節流會被讀入 方法區
中進行存儲,所以後續的 3 個驗證階段都是基於方法區中的存儲結構,不再操作字節流。
驗證的內容:
- 是否以魔數
0xCAFEBABE
開頭
0xCAFEBABE(咖啡寶貝?)
是 Java 編譯後 Class 文件的開頭標識,有這個說明是 Java 的 Class 文件 - 主、次版本號是否在當前虛擬機的處理範圍內
不少人應該都遇到過,由於 JDK 版本太低,導致的major.minor version 52.0
,百度下發現要用 JDK8 - 常量池中的常量是否有不被支持的類型(檢查常量 tag 標誌)
- CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 編碼的數據
- Class 文件 中各個部分及文件本身是否有被刪除的或附加的其他信息
- … …
元數據驗證
對類的元數據進行驗證,保證 不存在不符合 Java 語言規範的元數據信息
。
驗證內容:
- 這個類是否有父類(除了 Object 類都應該有父類)。
- 這個類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)。
- 如果這個類不是抽象類,是否實現了
父類
或接口
中要求實現的所有方法。 - 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的 final 字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不同等)。
字節碼驗證
最複雜的階段,通過數據流和控制流的分析,確認軟件語義是 合法
、符合邏輯
的,保證被校驗類的方法 在運行時不會做出危害虛擬機安全的事件
。
例如:
- 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作。
- 保證指令不會跳到方法體以外的字節碼指令上。
- 保證方法體中的類型轉換是有效的。
- … …
符號引用驗證
驗證虛擬機將 符號引用
轉化爲 直接引用
時,進行的 匹配性校驗
。
驗證內容:
- 符號引用中通過
全限定名
是否能找到對應的類。 - 在指定類中是否存在
符合方法的字段描述
以及簡單名稱所描述的方法和字段
。 - 符號引用的中的類、字段、方法的訪問性(private、protected、public、default)是否可以被當前類訪問。
- … …
準備
準備階段
是正式爲 類變量分配內存並設置類變量初始值
的階段,這些變量所使用的內存都將在方法區中進行分配
初始值爲零值:
這時候進行內存分配的僅包括 類變量(被static修飾的變量)
,而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在 Java 堆中
其次,這裏所說的初始值 “通常情況” 下是數據類型的 零值
,假設一個類變量的定義爲:
public static int value=123;
那變量 value
在 準備階段過後的初始值爲 0 而不是 123
而把 value
賦值爲 123
的 putstatic
指令是程序被編譯後,存放於類構造器 <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>(),其他的阻塞等待
。