JVM_05虛擬機加載機制

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

一、類加載的時機

類從加載到虛擬機內存開始,到卸載出內存結束,生命週期:加載、連接(驗證、準備、解析)、初始化、使用、卸載。

注意:加載、驗證、準備、初始化、卸載這五個時嚴格按順序開始的,但解析的過程有時會在初始化之後,這是爲了支持Java的動態綁定。

1. 主動引用

對於初始化,虛擬機規定有且只有四種情況必須立即對類進行初始化工作(加載、驗證、準備在此之前):

1)遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化

2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則先觸發初始化

3)當初始化某個類,發現其父類還未進行初始化

4)當虛擬機啓動時,用戶需要指定一個要執行的主類,虛擬機會先初始化這個主類

注意:如果是接口的話,在初始化時,並不要求其父接口全部完成初始化,只有在真正使用到父接口的時候(引用接口中定義的常量)纔會初始化。

2. 被動引用

引用類,卻不會觸發初始化

1)通過子類引用父類的靜態字段,不會導致子類初始化

2)通過數組定義來引用類,不會觸發此類的初始化

3)常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

二、類的加載過程

加載、驗證、準備、解析、初始化這五個階段

1. 加載

1)通過一個類的全限定名來獲取定義此類的二進制字節流

2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構

3)在Java堆中生成了一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口

加載完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,方法區中的數據存儲格式由虛擬機自行定義,虛擬機規範未規定此區域的具體數據結構。

然後再Java堆中實例化一個java.lang.Class類對象,這個對象將作爲程序訪問方法區中的這些類型數據的外部接口。

2. 驗證

驗證是連接的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機自身的安全。

Java本身是一個相對安全的語言,訪問數組邊界以外、將對象轉型爲其未實現類型、跳轉到不存在的代碼行等操作,都會被編譯器拒絕編譯;但通過十六進制編譯器直接產生Class文件,卻可以在字節碼語言層面實現Java代碼無法做到的事情。虛擬機如果不進行字節流檢查,可能因載入有害的輸入字節流而導致系統崩潰,所以驗證是必要的。

驗證過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。

1)文件格式驗證

保證輸入的字節流能夠正確的解析並存儲於方法區內。

## 是否以魔數0xCAFEBABE開頭

## 主次版本號是否在當前虛擬機處理範圍內

## 常量池的常量中是否有不支持的常量類型(檢查tag標誌)

……

2)元數據驗證

對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言的規範要求。對類的原數據信息進行語義校驗,保證不存在不符合JAVA語言規範的元數據信息。

## 這個類是否有父類(除java.lang.Object外,都應有父類)

## 這個類的父類是否繼承了不允許被繼承的類(被final修飾的)

## 如果該類不是抽象類,是否實現了其父類或接口之要求的實現的所有方法

……

3)字節碼驗證

進行數據流和控制流分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行爲。

## 保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作

## 保證跳轉指令不會跳轉到方法體以外的字節碼指令上

## 保證方法體中的類型轉化是有效的

4)符號引用驗證

發生在JVM將符號引用轉化爲直接引用的時候,這個轉化動作發生在解析階段,符號引用驗證可以看做是對類自身以外的信息進行匹配性的校驗。

## 符號引用通過字符串描述的全限定名是否能找到對應類

## 在指定類中是否存在方法的字段描述符及簡單名稱所描述的方法和字段

## 符號引用中的類、字段和方法的訪問性是否可以被當前類訪問

3. 準備

是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。

這裏只是將類變量進行內存分配,而不包括實例變量;初始化通常是數據類型的零值。

4. 解析

JVM將常量池內的符號引用替換爲直接引用的過程。

1)符號引用

一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義定位到目標即可;與虛擬機實現的內存佈局無關;引用目標不一定已經加載到內存中。

2)直接引用

直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄;與虛擬機實現的內存佈局相關;引用目標必定在內存中存在。

解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用進行,分別對應於常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四種常量類型。

1)類或接口的解析

假設當前代碼所處的類爲D,如果要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,那虛擬機完成整個解析過程需要包括以下3個步驟:

1# 如果C不是一個數組類型,那JVM將會把代表N的全限定名傳遞給D的加載器去加載這個類。一旦加載過程出現異常,解析過程宣告失敗。

2# 如果C是一個數組類型,並且數組的元素類型也爲對象,也就是N的描述符會是類似“[Ljava.lang.Integer”的形式,將按照第1點規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載元素類型爲“java.lang.Integer”,接着由JVM生成一個代表此數組維度和元素的數組對象。

3# 如果上面步驟無任何異常,那麼C在JVM中實際上已經具備對D的訪問權限;如果發現不具備訪問權限,拋出java.lang.IllegalAccessError異常。

2)解析字段

要解析一個未被解析過的字段符號引用,首先將會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所述的類或接口的符號引用。如果在解析這個類或接口符號引用的過程出現任何異常,都會導致字段符號引用解析失敗。

如果解析成功,將這個字段所屬的類或接口用C表示,對C進行後續字段搜索:

1# 如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。

2# 否則,如果在C中實現了接口,將會按照繼承關係從上往下遞歸搜索各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。

3# 否則,如果C不是java.lang.Object的話,將會按照繼承關係從上往下遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。

4# 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。

如果查找過程成功返回了引用,將會對這個字段進行權限驗證,如果發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。

3)類方法解析

與字段解析一樣,先解析出類方法表的class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功,用C表示該類,接下來的類方法搜索:

1# 類方法和接口方法符號引用的常量類型定義是分開的,如果在類方法表中發現class_index中索引的C是個接口,直接拋出java.lang.IncompatibleClassChangeError異常。

2# 如通過第1步,在類C中查找是否有簡單名稱和描述符鬥魚目標相匹配的方法,有則返回這個方法的直接引用,查找結束。

3# 否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,有則返回這個方法的直接引用,查找結束。

4# 否則,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,此時查找結束,拋出java.lang.AbstractMethodError異常。

5# 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。

如果查找過程成功返回了引用,將會對這個方法進行權限驗證,如果發現不具備對方法的訪問權限,將拋出java.lang.IllegalAccessError異常。

4)接口方法解析

也是需要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功,用C表示該接口,接下來的接口方法搜索:

1# 與類方法解析相反,如果在接口方法表中發現class_index中的索引C是個類而不是接口,就直接拋出java.lang.IncompatibleClassChangeError異常。

2# 否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回該方法的直接引用,查找結束。

3# 否則,在接口C的父接口中遞歸查找,直到java.lang.Object類位置,看是否有簡單名稱和描述符都與目標相匹配方法,有則返回該方法的直接引用,查找結束。

4# 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

接口中的所有方法都默認爲public的,不存在訪問權限問題,其解析不會拋出java.lang.IllegalAccessError異常。

5. 初始化

類初始化是類加載過程中的最後一步,在初始化階段,才真正開始執行類中定義的Java程序代碼。

在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,換句話說:初始化階段是執行類構造器<clinit>()方法的過程。

## <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的

三、類加載器

把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到JVM外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱爲“類加載器”。

1. 類與類加載器

對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。也就是說比較兩個類是否相等,需要在兩個類是由同一個類加載器加載的前提下才有意義。

這裏的“相等”包括:代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關係判定等情況。

2. 雙親委派模型

從JVM角度來講,只有兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種是所有的其他的類加載器,這些類加載器由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。

絕大部分Java程序都會使用到以下三種系統提供的類加載器:

1)啓動類加載器(Bootstrap ClassLoader):負責將存放在<JAVA_HOME>\lib目錄中的,並且是虛擬機識別的類庫,加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用。

2)擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄中的,或被系統變量指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。

3)應用程序類加載器(Application ClassLoader):負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有定義過自己的類加載器,一般情況下這個就是程序中的默認加載器。

雙親委派模型要求除頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器;但類加載器的父子關係並不是繼承,而是通過組合複用父類加載器代碼。

雙親委派模型工作過程:

如果一個類加載器收到了類加載的請求,首先不會自己去嘗試記載該類,而是把這個請求委派給父類加載器去完成,因此所有的加載請求都會傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試加載。

優點:保證了無論加載那個類都會先委派給啓動類加載器,不會出現多個同名類,造成混亂。

實現:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父類加載器爲空默認使用啓動類加載器作爲父加載器。如果父類加載失敗,則在拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章