JVM 之類加載機制
作者:Lebens
鏈接:https://www.jianshu.com/p/271f37bcd424
一個 Java 類從被加載到虛擬機內存到被卸載出內存爲止,生命週期一共包括如下幾個階段:
-
加載(Loading)
-
驗證 (Verfication)
-
準備 (Preparation)
-
解析 (Resolution)
-
初始化 (Initialization)
-
使用 (Using)
-
卸載 (Unloading)
其中驗證、準備、解析這個 3 個部分統稱爲鏈接(Linking)。
image
加載、驗證、準備、初始化和卸載這 5 個階段開始執行的順序是一定的,但不意味着這幾個階段是分開執行的,這些階段通常是相互交叉混合式進行的,通常會在一個階段執行的過程中調用、激活另一個過程。
解析階段則不一定,爲了支持 java 的運行時綁定,在某些特定的情況下解析可以在初始化階段之後開始。
加載階段
這個階段主要完成如下 3 件事情
-
通過一個類的全限定名來獲取定義此類的二進制字節流
-
將這個字節流代表的靜態存儲結構轉化成方法區的運行時數據結構
-
生成一個此類的 Java.lang.Class 對象(方法區中),作爲方法區這個類的數據的訪問入口。
這裏獲取字節流的方式並不侷限於 zip,還包括諸如網絡中獲取、運行時生成、其他文件生成、數據庫讀取等方式。
同時相對於類加載過程的其他階段,相對於一個非數組類的加載階段是開發者可控性最強的,因爲加載階段既可以使用系統提供的加載器,也可以用戶自定義類加載器來完成類的加載。
數組類的加載情況有所不同,雖然數組類是 JVM 直接創建的,但是數組的組件,最終還是要依靠類加載器去加載,一個數組類創建主要有如下幾點:
-
如果數組的組件類型爲引用類型,數組將會在組件的類加載器上被標識。
-
如果組件類型不是引用類型,JVM 會將數據將會與引導類加載器關聯。
-
數組的可見性與它的組件類型保持一致,如果組件的類型不是引用類型,則數據可見性默認爲 public。
加載階段完成後,類的二進制字節流將按照 JVM 所需的格式存儲在方法區中,同時在內存中實例化一個 java.lang.Class 的實例對象,作爲程序訪問方法區中這些類數據的外部接口。相對於 HotSpot,這個實例對象比較特殊,雖然是一個對象,但並沒有放置在堆中,而是放置在方法區中。
驗證階段
這個階段主要是確保加載的 Class 文件中的字節流包含的信息符合當前虛擬機的要求,同時不存在損害 JVM 的行爲.
從整體上看,驗證階段大致上會完成下面 4 個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證
文件格式驗證
這一階段主要驗證字節流是否符合 Class 文件格式的規範:
-
驗證開頭的 4 字節的 Magic Number 是否是 0xCAFEBABE
-
檢驗當前的主、次版本號能否能被當前虛擬機處理
-
常量池中是否有不被支持的常量類型
-
指向常量的各種索引值是否有不存在或者不符合類型的常量
-
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據
-
Class 文件的各個部分以及文件本身是否被刪除或者附帶其他信息
以及等等一些步驟,通過這個步驟的驗證之後,字節流纔會進入方法區中存儲,後續的 3 個階段的驗證,都是基於方法區的存儲結構進行的
元數據驗證
這一階段主要是對字節碼描述的信息進行語義分析:
-
是否有父類(只有 Object 沒有父類)
-
它的父類是否繼承了不允許被繼承的類(被 final 修飾的類)
-
非抽象類是否實現了父類要求實現的方法
-
成員變量、方法等是否與父類相矛盾(覆蓋父類 final 字段,重載的方法是否符合要求)
等等
字節碼驗證
這一階段將對類的方法體進行校驗分析,保證類的方法在運行時不會做出危害虛擬機安全的事件。這個階段是整個驗證過程中最複雜的一個階段
-
保證任意時刻操作數棧的數據類型都能和指令代碼序列配合工作
-
保證跳轉指令不會跳轉到方法體之外的指令
-
保證方法體中的類型轉換是有效的
等等
沒有通過字節碼驗證的方法體一定是有問題的,但是通過字節碼驗證的方法體也不能保證一定沒問題。
符號引用驗證
這個驗證發生在 JVM 將符合引用轉換成直接引用的時候,在鏈接的第三階段 --- 解析階段發生
-
符合引用通過字符串描述的全限定名能否找到對應的類
-
指定類中是否存在符合方法的字段描述以及簡單名稱所描素的方法和字段
-
符號引用的類、字段、方法是否能被當前類訪問
等等
驗證階段對於 JVM 的類加載機制是非常重要的,但不是必要的,因爲對程序運行期沒有影響
準備階段
這個階段會執行類的 (),主要是爲類變量(也就是被 static 修飾的)分配內存並賦值零值的階段。所謂的零值就是默認的初始值,每種數據類型各有不同的零值(需要注意的是同時被 static final 修飾的變量在此時就賦值完畢,並不會存在賦零值的操作)。類的是編譯器自動收集類變量以及靜態代碼塊後自動合併生成的。
-
() 對於類和接口來說這個方法並不是必須的。
-
() 中,靜態語句只能訪問定義在它之前定義的靜態變量,定義在它之後的靜態變量,可以賦值,但不能訪問。
-
子類
() 不需要顯示的調用父類的構造器,JVM 保證子類的 () 執行之前,父類的 () 已經執行完畢。
-
由於父類的
() 先執行,所以父類的靜態語句優先與子類的靜態語句執行
-
先對類,接口的執行
() 時並不需要執行父接口的 () 方法,只有使用父接口定義的變量時,父接口才會初始化。接口的實現類初始化時也不會調用接口的 ()
-
JVM 保證一個類的
() 執行時線程安全的,多線程執行類的 () 時只能有一個被執行,其餘線程等待(執行完畢後其他線程不再進入 ())。如果一個類的 () 執行耗時操作,可能會造成多進程阻塞
舉個栗子:
public static int abc = 123;
public static final int ABC = 123;
上面兩句的代碼在準備階段的區別是
-
準備階段過後,變量 abc 的初始值是 0,而不是 123。被 static 修飾的變量賦值操作,被 JVM 收集後存在於類構造器的
() 中,這裏還沒執行到類的初始化階段,所以並不會被賦值成 123;
-
同時被 static final 修飾的變量 ABC 在編輯階段 javac 就會爲 ABC 生成 ConstantValue 屬性,在準備 JVM 就會根據 ConstantValue 直接賦值,也就是準備階段後 ABC 的值爲 123;
解析階段
-
符號引用:符合引用是以一組符號來描述所引用的目標,符號可以可是以任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用的對象並不一定加載到內存中
-
直接引用:直接引用是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。相對於符號引用,被直接引用的對象一定已經加載到內存中。
類或接口的解析
-
C 是非數組類型,JVM 通過 N 代表的全限定名傳遞給 D 的類加載器去加載 C。此過程中可能出去 C 繼承的父類或實現的接口的加載
-
C 是數組類型,同時元素類型是引用類型,通過上面步驟去加載數組元素的引用類型,並由 JVM 生成一個代表數組 C 維度和元素的數組對象
-
解析完成之後進行符號引用驗證,確認 D 對 C 的引用權限
字段解析
-
若 C 中包含了簡單名稱和字段描述都符合的字段,則返回這個字段的直接引用
-
若 C 中實現了接口,則按照繼承關係從下往上搜索各個接口及其父接口,如匹配到簡單名稱和字段描述都一致的字段,則返回該字段的直接引用
-
若 C 不是 java.lang.Object,按照繼承關係從下往上搜索父類,如匹配到簡單名稱和字段描述都一致的字段,則返回該字段的直接引用
-
查找失敗,拋出 java.lang.NoSuchFieldError
-
解析完成後對字段進行權限驗證
類方法解析
-
若解析發現 C 是一個接口,則拋出異常
-
剩餘步驟和字段解析一致,不再贅述
接口方法解析
-
若解析返現 C 是一個類,則拋出異常
-
由於接口中所有的方法都是 public 的,省去權限校驗這一步驟
初始化階段
編譯器自動收集實例變量初始化以及實例代碼塊後自動合併生成類的 ()
子類初始化時會先調用父類 (),用以保證子類能正常初始化。
執行子類的 ()
使用階段
這個沒啥好說的,只要代表類的 Class 對象還能被引用到,類就還在使用當中。
卸載階段
一個類何時結束生命週期,取決於代表它的 Class 對象何時結束生命週期。類的卸載,其實就是方法區中的類的回收。判斷一個類是否可以進行回收,需要同時滿足下面 3 個條件:
-
該類所有的實例對象都已經被回收,java 堆中不存在任何該類的實例對象
-
加載該類的 ClassLoader 已經被回收
-
該類的 java.lang.Class 對象沒有在任何地方被引用到,無法在任何地方通過反射訪問到該類的方法。
上面條件僅僅說明該類可以進行回收,但是並不想類的實例對象一樣,不使用了就立馬進行回收。
如上圖所示,當左側所有的引用都消失時,類可以被回收。
Java 虛擬機自帶的類加載器包括根類加載器、擴展類加載器和系統類加載器。由 Java 虛擬機自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。Java 虛擬機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的 Class 對象,因此這些 Class 對象始終是可觸及的。由用戶自定義的類加載器加載的類是可以被卸載的。
參考書籍
本文摘錄、整理自周志明的《深入理解 Java 虛擬機》一書,如想獲得更詳細介紹可自己查閱此書。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。