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