Jvm類的載入機制
1.概述
虛擬機器載入Class檔案(二進位制位元組流)到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可被虛擬機器直接使用的Java型別,這一系列過程就是類的載入機制。
2.類的載入時機
類從被虛擬機器載入到記憶體開始,直到卸載出記憶體為止,整個生命週期包括:載入——驗證——準備——解析——初始化——使用——解除安裝 這7個階段。其中驗證、準備、解析3個部分統稱為連線。
生命週期圖如下:
其中載入、驗證、準備、初始化、解除安裝這5個階段順序是確定的,類的載入過程必須按照這種順序進行開始,而解析階段則不一定:它在某種情況下可以在初始化之後再開始,這也是為了支援Java語言的動態繫結。
哪些情況能觸發類的初始化階段?(前提:載入、驗證、準備自然是已經執行完了)
- 遇到new、getstatic、putstatic、invokestatic 這4條指令時如果類沒有初始化則會觸發其初始化,(工作中觸發這4種指令最常見的場景:new例項化物件、讀取or設定類的靜態欄位【final修飾或者已經把靜態欄位放入常量池的除外】、呼叫類的靜態方法)
- 使用反射的時候
- 初始化類的時候如果其父類還沒進行初始化,則需要先觸發父類的初始化
- 虛擬機器啟動時,需要指定一個要執行的主類(包含main方法的那個類),虛擬機器會先初始化這個類
- 使用jdk1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼。切這個控制代碼對應的類沒有初始化,則需要先觸發其初始化
注意:所有引用類的方式都不會觸發初始化(被動引用)例如:建立陣列、引用final修飾的變數、子類引用父類的靜態變數 不會觸發子類初始化但是會觸發父類初始化
3.類的載入過程
- 載入
載入是類載入的一個階段,在載入階段 虛擬機器需要完成下面3件事情
- 通過類的全限定名來獲取定義此類的二進位制位元組流
- 將這個位元組流所代表的靜態儲存結構轉化成方法區的執行時資料結構
- 在記憶體中生成一個代表此類的java.lang.Object物件,作為方法區這個類的各種資料的訪問入口
相對於類載入的其他階段,載入階段(準確的說,是載入階段中獲取類的二進位制位元組流的動作)是開發人員可控性最強的。因為載入階段既可以使用系統提供的引導類載入器來完成,也可以由開發人員自定義的類載入器來完成(即重寫類載入器的loadClass()方法)。
載入完成後,外部的二進位制位元組流就轉化成虛擬機器所需的格式儲存在方法區中,然後在記憶體中例項化一個java.lang.Class類的物件。這個物件將作為程式訪問方法區中的這些型別資料的外部介面。
載入階段與連線階段的部分內容是交叉進行的,並不是載入完成後才能執行驗證等操作。這些夾在載入之中的動作仍然屬於連線階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。
- 驗證
驗證是連線的第一步,為了保證載入的二進位制位元組流所包含的資訊是符合虛擬機器規範的。
驗證階段大致分為下面4個檢驗動作:
檔案格式驗證:驗證位元組流是否符合Class檔案格式規範。例如:是否以魔數 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機器處理範圍內、常量池中的常量是否有不被支援的型別······。
元資料驗證:對位元組碼描述的資訊進行語義分析。例如: 這個類是否有父類、是否正確的繼承了父類。
位元組碼驗證:通過資料流和控制流的分析,確定程式語義是合法的、符合邏輯的(說白了就是對類的方法體進行分析確保方法在執行時不會危害虛擬機器)。
符號引用驗證:確保解析動作能正常執行。
驗證階段是非常重要,但不一定是必要的階段(因為對程式執行期沒有影響)。如果所執行的全部程式碼都已經被反覆使用和驗證過,那麼在實施階段可以使用-Xverify:none引數來關閉驗證。
- 準備
正式為類變數分配記憶體並設定類變數初始值。這些變數所使用的記憶體都將在方法區中進行分配。
注意:
- 此時被分配的僅僅是靜態變數,而不是例項變數,例項變數將隨著物件例項一起分配在Java堆中
- 初始值通常情況下是資料型別的零值。假如定義一個靜態變數 public static int value = 123;那麼value在準備階段初始值為0而不是123。
- 被final修飾的變數在準備階段就初始化為屬性所指定的值。例如: public static final int value = 123;那麼value在準備階段初始值就是123。
- 解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。
符號引用:以一組符號來描述引用的目標,符號可以是任何形式的字面量。
直接引用:指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。
- 初始化
初始化階段是執行類構造器<clinit>()方法的過程。在準備階段,變數已經賦過一次系統要求的初始值,而在初始化階段,則根據程式設計師制定的引數值去初始化類變數和其他資源。
類構造器<clinit>()方法:是由編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併產生的。
編譯器收集的順序是由語句在原始檔中出現的順序決定的;靜態程式碼塊只能訪問定義在靜態塊之前的變數,定義在它之後的變數,在前面的靜態塊中可以賦值,但不能訪問。
非法向前引用示例 public class SuperClass { public static int va; static { value = 1; //可以編譯通過 va = value; //報錯 非法向前引用 System.out.println("父類初始化"); } public static int value = 123; }
<clinit>()方法 對類或介面來說並不是必須的,如果一個類中沒有靜態程式碼塊,也沒用對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>方法
介面中不能使用靜態塊,但仍可以有變數賦值操作,因此介面和類一樣都會生成<clinit>方法。不同的是,介面初始化不需要先執行父類的初始化,只有當父介面中的變數使用時,才會觸發父介面的初始化。另外介面的實現類也不會觸發介面的例項化。
虛擬機器會保證一個類的<clinit>()方法在多執行緒中被正確的加鎖、同步,如果多個執行緒去初始化一個類,那麼只會有一個執行緒去執行類的<clinit>()方法,其他執行緒都處於等待狀態。只能活動執行緒執行完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個執行緒阻塞,在實際應用中這種阻塞往往是很隱蔽的。
4.類載入器
虛擬機器設計團隊把類載入中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼塊稱為類載入器。
從Java開發人員的角度看,類載入器大致分為如下3種
啟動類載入器(Bootstrap Classloader):負責將存放在<JAVA_HOME>\lib(Javahome即jdk的安裝目錄)目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib下面也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接使用。
擴充套件類載入器(Extension Classloader):該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的系統路徑中的所有類庫。開發者可以直接使用擴充套件類載入器。
應用程式類載入器(Application Classloader):該載入器由sun.misc.Launcher$AppClassLoader實現,它負責載入使用者類路徑(ClassPath)上所指定的類庫。開發者可以直接使用此載入器。如果應用程式中沒有自定義的類載入器,那麼這個就是程式預設執行的類載入器。(系統載入器)
我們的應用程式都是由這3種類載入器相互配合進行載入的。如果有必要,還可以加入自定義的類載入器。
這些類載入器之間的關係如下圖:
5.雙親委派模型:
雙親委派模型的工作過程是:如果一個類載入器收到了一個類載入請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一層的載入器都是如此,因此所有的載入請求最終都應該到達頂層的啟動類載入器。只有當父載入無法完成這個載入請求時,子載入器才會嘗試自己去載入。
雙親委派機制:
1、當ApplicationClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
3、如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。
ClassLoader原始碼分析:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先檢查此類是否已被載入 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //委派給父類載入器去載入 if (parent != null) { c = parent.loadClass(name, false); } else { //如果沒有父載入器,則呼叫啟動類載入器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //如果父載入器無法載入,則呼叫本身載入器去載入 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
雙親委派模型意義:
- 系統類防止記憶體中出現多份同樣的位元組碼
- 保證Java程式安全穩定執行
參考
《深入理解Java虛擬機器》
https://www.cnblogs.com/ityouknow/p/5603287.html