詳解JVM虛擬機的類加載機制
1、什么是虛擬機的類加載機制?
虛擬機把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型。
這就是虛擬機的類加載機制(通俗一點講就是把 Class 轉(zhuǎn)成虛擬機可以使用的 Java 類型,轉(zhuǎn)換過程中會有校驗、解析和初始化的操作)。
2、類的生命周期:
類從被加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存為止,它的生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段。其中,驗證、準備、解析三個部分統(tǒng)稱為連接(Linking)。如下圖所示:
圖中需要注意的地方:加載、驗證、準備、初始化和卸載這五個階段具有確定的順序。
這個確定的順序是指"開始",即驗證階段必須等加載開始之后才能開始,準備階段開始必須在驗證開始之后。而 “進行” 和 “完成” 的順序則不一定,這些階段通常都是可以互相交叉地混合式進行的,通常會在一個階段執(zhí)行的過程中調(diào)用、激活另外一個階段。
3、觸發(fā)類初始化的條件(有且只有五種情況):
- new 一個對象的時候、讀取或設置一個類的靜態(tài)字段(放入常量池的靜態(tài)字段除外:被 final 修飾的 static 變量)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 反射調(diào)用的時候。
- 子類初始化觸發(fā)父類初始化。
- 執(zhí)行的主類(包含 main() 方法的那個類)。
- 當使用 JDK 1.7 的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄的時候。
下面幾個例子是類的被動引用,不會觸發(fā)類的初始化:
(1)通過子類引用父類的靜態(tài)字段,不會導致子類初始化
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value);//通過子類引用父類的靜態(tài)字段,不會導致子類初始化 } }
(2)通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
public class NotInitialization { public static void main(String[] args) { SuperClass[] superClassArray = new SuperClass[10]; // 通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化 } // 這段代碼不會輸出 "SuperClass init!",即沒有觸發(fā) SuperClass 的初始化 }
(3)常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLO_WORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLO_WORLD); // 引用常量池的變量不會觸發(fā)定義常量的類的初始化 } // 不會輸出 "ConstClass init!" }
4、類加載的過程 加載
- 加載階段,虛擬機通過一個類的全限定名來獲取定義此類的二進制流,然后把二進制流的靜態(tài)存儲結構轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結構,最后在內(nèi)存中生成一個對象,作為方法區(qū)這個類各種數(shù)據(jù)的訪問入口。
- 驗證(非常重要但非必要)
驗證的目的在于確保 Class 文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。(假如自己編寫的及第三方庫都已經(jīng)被反復使用和驗證過,那么,可以考慮使用 -Xverify:none 參數(shù)關閉大部分的類驗證機制,以縮短虛擬機類加載的時間)
驗證階段大致會完成以下四個驗證動作:
(1)文件格式驗證
(2)元數(shù)據(jù)驗證
(3)字節(jié)碼驗證
(4)符號引用驗證
- 準備
準備階段是給類成員變量設置初始值(數(shù)據(jù)類型的零值)。
這里的類成員變量指被 static 修飾的變量;初始值指數(shù)據(jù)類型的零值;而常量(被 static final 修飾的變量)會初始化為具體的值。
例如:
public static final int value = 123; // 虛擬機在準備階段就會根據(jù) ConstantValue 的設置將 value 賦值為 123。
數(shù)據(jù)類型的零值:
- 解析
解析階段主要做的事情是虛擬機將常量池內(nèi)的符號引用替換成直接引用。
Java 中編譯時 java.lang.NoSuchFieldError
、 java.lang.NoSuchMethodError
、 java.lang.IllegalAccessError
等異常就是在解析階段拋出的。
- 初始化
初始化階段是類加載過程的最后一步。前面我們提到,準備階段會給類變量設置初始零值,而在初始化階段,則是給類變量執(zhí)行代碼里真正賦值的時候。
- 賦值操作由所有類變量的賦值動作和靜態(tài)語句塊中的語句共同產(chǎn)生,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問,例如:
public class Test { static { i = 0; // 靜態(tài)語句塊可以給后面定義的類變量賦值,但是不能訪問(編譯器會報 "非法向前引用" ) // System.out.println(i); Illegal forward reference. } static int i = 1; }
初始化階段的注意事項:
(1)靜態(tài)成員變量初始化在靜態(tài)方法之前( static {} 中都是變量賦值);
(2)父類初始化在子類之前
(3)執(zhí)行接口的初始化的時候不需要先執(zhí)行父接口的初始化,只有當父接口中定義的變量使用時,父接口才會初始化;
(4)虛擬機會保證一個類的初始化方法在多線程環(huán)境中被正確地加鎖、同步:保證了在同一個類加載器下,一個類型只會初始化一次。
到此這篇關于詳解JVM虛擬機的類加載機制的文章就介紹到這了,更多相關JVM類加載機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringCloud中的Feign遠程調(diào)用接口傳參失敗問題
這篇文章主要介紹了SpringCloud中的Feign遠程調(diào)用接口傳參失敗問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03實例講解Java的Spring框架中的AOP實現(xiàn)
這篇文章主要介紹了Java的Spring框架中的AOP實現(xiàn)實例,AOP面向切面編程其實也可以被看作是一個設計模式去規(guī)范項目的結構,需要的朋友可以參考下2016-04-04Java項目Guava包?HashMultimap使用及注意事項
guava基本上可以說是java開發(fā)項目中,大概率會引入的包,今天介紹的主角是一個特殊的容器HashMultmap,可以簡單的將它的數(shù)據(jù)結構理解為Map<K,?Set<V>>,今天主要介紹下基礎的知識點?HashMultmap級使用,感興趣的朋友一起看看吧2022-05-05