文章目錄
- 前言
- 1.加載
- 2.鏈接
- 驗證
- 文件格式驗證
- 元數據驗證
- 字節碼驗證
- 符號引用驗證
- 準備
- 解析
- 3.初始化
- 4.類卸載
前言
類從被加載到虛擬機內存中開始到卸載出內存為止,它的整個生命周期可以簡單概括為 7 個階段:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)。其中,驗證、準備和解析這三個階段可以統稱為連接(Linking)。
這 7 個階段的順序如下圖所示:
注意:系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分為三步:驗證->準備->解析。
1.加載
類加載過程的第一步,主要完成下面 3 件事情:
- 通過全類名獲取定義此類的二進制字節流。
- 將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構。
- 在內存中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口。
加載這一步主要是通過我們后面要講到的 類加載器 完成的。類加載器有很多種,當我們想要加載一個類的時候,具體是哪個類加載器加載由 雙親委派模型 決定。
每個 Java 類都有一個引用指向加載它的 ClassLoader。不過,數組類不是通過 ClassLoader 創建的,而是 JVM 在需要的時候自動創建的,數組類通過getClassLoader()方法獲取 ClassLoader 的時候和該數組的元素類型的 ClassLoader 是一致的。
一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass() 方法)。
加載階段與連接階段的部分動作(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。
將類的字節碼載入方法區中,內部采用 C++ 的 instanceKlass 描述 java 類,它的重要 field 有:
- _java_mirror 即 java 的類鏡像,例如對 String 來說,就是 String.class,作用是把 klass 暴露給 java 使用
- _super 即父類
- _fields 即成員變量
- _methods 即方法
- _constants 即常量池
- _class_loader 即類加載器
- _vtable 虛方法表
- _itable 接口方法表
注意 - instanceKlass 這樣的【元數據】是存儲在方法區(1.8 后的元空間內),但 _java_mirror是存儲在堆中
- 可以通過 HSDB 工具查看
2.鏈接
驗證
驗證是連接階段的第一步,這一階段的目的是確保 Class 文件的字節流中包含的信息符合《Java 虛擬機規范》的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全。
驗證階段這一步在整個類加載過程中耗費的資源還是相對較多的,但很有必要,可以有效防止惡意代碼的執行。任何時候,程序安全都是第一位。
不過,驗證階段也不是必須要執行的階段。可以在生產環境的實施階段就可以考慮使用 -Xverify:none 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
但是需要注意的是 -Xverify:none 和 -noverify 在 JDK 13 中被標記為 deprecated ,在未來版本的 JDK 中可能會被移除。
驗證階段主要由四個檢驗階段組成:
- 文件格式驗證(Class 文件格式檢查)
- 元數據驗證(字節碼語義檢查)
- 字節碼驗證(程序語義檢查)
- 符號引用驗證(類的正確性檢查)
文件格式驗證
文件格式驗證這一階段是基于該類的二進制字節流進行的,主要目的是保證輸入的字節流能正確地解析并存儲于方法區之內,格式上符合描述一個 Java 類型信息的要求。
除了這一階段之外,其余三個驗證階段都是基于方法區的存儲結構上進行的,不會再直接讀取、操作字節流了。
這一階段可能包括下面這些驗證點:
- 是否以魔數0xCAFEBABE開頭。
- 主、次版本號是否在當前Java虛擬機接受范圍之內。
- 常量池的常量中是否有不被支持的常量類型(檢查常量tag標志)。
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數據。
- Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息
- …
注意:方法區屬于是 JVM 運行時數據區域的一塊邏輯區域,是各個線程共享的內存區域。當虛擬機要使用一個類時,它需要讀取并解析 Class 文件獲取相關信息,再將信息存入到方法區。方法區會存儲已被虛擬機加載的 類信息、字段信息、方法信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等數據。
元數據驗證
第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合《Java語言規范》的要
求,這個階段可能包括的驗證點如下:
- 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)。
- 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
- 如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法。
- 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不同等)。
- ……
字節碼驗證
第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數據流分析和控制流分析,確定程序語義是合法的、符合邏輯的。
在第二階段對元數據信息中的數據類型校驗完畢以后,這階段就要對類的方法體(Class文件中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為,例如:
- 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現類似于“在操作棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中”這樣的情況。
- 保證任何跳轉指令都不會跳轉到方法體以外的字節碼指令上。
- 保證方法體中的類型轉換總是有效的,例如可以把一個子類對象賦值給父類數據類型,這是安全的,但是把父類對象賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關系、完全不相干的一個數據類型,則是危險和不合法的。
- ……
符號引用驗證
符號引用驗證發生在類加載過程中的解析階段,具體點說是 JVM 將符號引用轉化為直接引用的時候(解析階段會介紹符號引用和直接引用)。
符號引用驗證的主要目的是確保解析階段能正常執行,如果無法通過符號引用驗證,JVM 會拋出異常,比如:
- java.lang.IllegalAccessError:當類試圖訪問或修改它沒有權限訪問的字段,或調用它沒有權限訪問的方法時,拋出該異常。
- java.lang.NoSuchFieldError:當類試圖訪問或修改一個指定的對象字段,而該對象不再包含該字段時,拋出該異常。
- java.lang.NoSuchMethodError:當類試圖訪問一個指定的方法,而該方法不存在時,拋出該異常。
- ……
準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:
- 這時候進行內存分配的僅包括類變量( Class Variables ,即靜態變量,被 static 關鍵字修飾的變量,只與類相關,因此被稱為類變量),而不包括實例變量。實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。
- 從概念上講,類變量所使用的內存都應當在 方法區 中進行分配。不過有一點需要注意的是:JDK 7 之前,HotSpot 使用永久代來實現方法區的時候,實現是完全符合這種邏輯概念的。 而在 JDK 7 及之后,HotSpot 已經把原本放在永久代的字符串常量池、靜態變量等移動到堆中,這個時候類變量則會隨著 Class 對象一起存放在 Java 堆中。
- 這里所設置的初始值"通常情況"下是數據類型默認的零值(如 0、0L、null、false 等)。
比如我們定義了public static int value=111 ,那么 value 變量在準備階段的初始值就是 0 而不是 111(初始化階段才會賦值)。因為這時尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構造器()方法之中,所以把value賦值為123的動作要到類的初始化階段才會被執行。
特殊情況:如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量值就會被初始化為ConstantValue屬性所指定的初始值。
比如給 value 變量加上了 final 關鍵字public static final int value=111 ,那么準備階段 value 的值就被賦值為 111。
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符 7 類符號引用進行。
舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機為每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被調用。
綜上,解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者字段、方法在內存中的指針或者偏移量。
3.初始化
初始化階段是執行初始化方法 ()方法的過程,是類加載的最后一步,這一步 JVM 才開始真正執行類中定義的 Java 程序代碼(字節碼)。
注意: ()方法是編譯之后自動生成的。
對于 () 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因為 () 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起多個線程阻塞,并且這種阻塞很難被發現。
發生的時機
概括得說,類初始化是【懶惰的】
- main 方法所在的類,總會被首先初始化
- 首次訪問這個類的靜態變量或靜態方法時,會引發初始化
- 子類初始化,如果父類還沒初始化,會引發初始化
- 子類訪問父類的靜態變量,只會觸發父類的初始化
- Class.forName 會導致初始化
- new 會導致初始化
- 當一個接口中定義了 JDK8 新加入的默認方法(被 default 關鍵字修飾的接口方法)時,如果有這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
不會導致類初始化的情況
- 訪問類的 static final 靜態常量(基本類型和字符串)不會觸發初始化
- 類對象.class 不會觸發初始化
- 創建該類的數組不會觸發初始化
- 類加載器的 loadClass 方法
- Class.forName 的參數 2 為 false 時
4.類卸載
卸載類即該類的 Class 對象被 GC。
卸載類需要滿足 3 個要求:
- 該類的所有的實例對象都已被 GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被 GC
所以,在 JVM 生命周期內,由 jvm 自帶的類加載器加載的類是不會被卸載的。但是由我們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就好了,JDK 自帶的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 負責加載 JDK 提供的類,所以它們(類加載器的實例)肯定不會被回收。而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的。