簡介
代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。
與那些在編譯時需要進行連接工作的語言不通,在Java語言里,類型的加載、連接和初始化過程都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增加一些性能開銷,但是會為Java應用程序提供高度的靈活性,Java里天生可以動態擴展的語言特性就是依賴運行期動態加載和動態鏈接這個特點實現的。
類加載的時機
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、連接(驗證、準備、解析)、初始化、使用、卸載。
對于初始化階段,虛擬機規范規定了有且只有5種情況必須立即對類進行"初始化“(而加載、驗證、準備自然需要在此之前開始):
- 遇到new、getstatic、putstatic或者invokestatic這4條字節碼指令時們如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或者設置一個類的靜態字段,以及調用一個類的靜態方法時
- 使用java.lang.reflect包的方法對類進行放射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化
- 當初始化一個類的時候,如果發現其弗雷還沒有進行過初始化,則需要先觸發其父類的初始化
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK1.7的動態語言支持時,如果一個java.lang.invok.Methodhandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
類加載的過程
加載
在裝載階段,虛擬機需要完成以下3件事情
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
虛擬機規范中并沒有準確說明二進制字節流應該從哪里獲取以及怎樣獲取,這里可以通過定義自己的類加載器去控制字節流的獲取方式。
驗證
驗證的目的是為了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
- 文件格式的驗證
驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析并存儲于方法區之內。經過該階段的驗證后,字節流才會進入內存的方法區中進行存儲,后面的三個驗證都是基于方法區的存儲結構進行的。
- 元數據驗證
對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規范的元數據信息。
- 字節碼驗證
該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。
- 符號引用驗證
這是最后一個階段的驗證,它發生在虛擬機將符號引用轉化為直接引用的時候(解析階段中發生該轉化,后面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。
準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配。對于該階段有以下幾點需要注意:
- 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
- 這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
- 類或接口的解析
判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。
- 字段解析
對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系從上往下遞歸搜索其父類,直至查找
- 類方法解析
對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。
- 接口方法解析
與類方法解析步驟類似,知識接口不會有父類,因此,只遞歸向上搜索父接口就行了。
初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作全部由虛擬機主導和控制,到了初始化階段,才真正開始執行類中定義的Java程序代碼,在準備階段變量已經賦過一次系統要求的初始值,而在初始化階段則通過程序制定的主觀計劃去初始化變量和其他資源,從另一個角度理解就是執行類構造器<clinit>()方法的過程
- <clinit>()方法是由編譯器自動收集類中的所有變量的賦值動作和靜態語句塊中的語句合并產生的,他按照代碼中出現的順序收集,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在他之后的,在靜態語句塊中只能賦值不能訪問
public class Test {
static{
i = 1;//可以賦值
System.out.println(i);//不能訪問
}
static int i = 0;
}
- <clinit>()方法在執行之前必須保證自己父類的類構造器方法已經執行完畢,因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object
- 由于父類的<clinit>()方法優先執行,意味著父類中定義的靜態語句塊要優先于子類的變量賦值操作
- <clinit>()并不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
- 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法,但是接口與類不同的是,執行接口的<clinit>()方法不需要先執行父類接口的<clinit>()方法,只有父類接口中定義的變量使用時父類接口才會初始化,另外接口實現類在初始化時也一樣不會執行接口的<clinit>()方法
- 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖,同步
類加載器
類加載器負責將 .class 文件加載到內存中,并為之生成對應的 Class 對象。
在JVM中,一個類用其全限定類名和其類加載器作為唯一的標識。這樣保證同一個類不會再次被載入。
類加載器分類:
不同的類加載器負責加載不同的類。主要分為兩類。
啟動類加載器(Bootstrap ClassLoader):由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中,即負責加載Java的核心類。
其他類加載器:由Java語言實現,繼承自抽象類ClassLoader。如:
擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫,即負責加載Java擴展的核心類之外的類。
應用程序類加載器(Application ClassLoader):負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器,通過ClassLoader.getSystemClassLoader()方法直接獲取。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。
雙親委派模型
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
graph LR
自定義類加載器-->應用程序類加載器
應用程序類加載器-->擴展類加載器
擴展類加載器-->啟動類加載器
這樣的好處是不同層次的類加載器具有不同優先級,比如所有Java對象的超級父類java.lang.Object,位于rt.jar,無論哪個類加載器加載該類,最終都是由啟動類加載器進行加載,保證安全。即使用戶自己編寫一個java.lang.Object類并放入程序中,雖能正常編譯,但不會被加載運行,保證不會出現混亂
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) {
//當父類的加載器不為空,則通過父類的loadClass來加載該類
c = parent.loadClass(name, false);
} else {
//當父類的加載器為空,則調用啟動類加載器來加載該類
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父類的類加載器無法找到相應的類,則拋出異常
}
if (c == null) {
//當父類加載器無法加載時,則調用findClass方法來加載該類
long t1 = System.nanoTime();
c = findClass(name); //用戶可通過覆寫該方法,來自定義類加載器
//用于統計類加載器相關的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//對類進行link操作
resolveClass(c);
}
return c;
}
}