虛擬機器類載入機制_類載入的過程

z1340954953發表於2018-04-16

Java虛擬機器中類載入的全過程: 載入、驗證、準備、解析和初始化這5個階段

載入

載入時類載入過程的一個階段,在載入階段,虛擬機器需要完成3件事

1> 通過一個類的全限定名來獲取定義此類的二進位制位元組流

2> 將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構

3> 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

(對於HotSpot虛擬機器而言,Class物件比較特殊,雖然是物件,但是存放在方法區裡面)

通過全限定名來獲取定義二進位制檔案這條,有多種方式實現

* 從zip包中讀取,很常見,最終成為日後jar、ear、war格式的基礎

* 從網路中獲取

* 執行時計算生成,這個場景使用較多的是動態代理

* 由其他檔案生成,典型的場景就是jsp應用,有jsp檔案生成對應class類

* 資料庫中讀取

驗證

1. 檔案格式驗證: 驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理

2. 後設資料驗證

對位元組碼描述的資訊進行語義分析,是否符合java語言規範

驗證點:

1> 是否有父類

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

3> 如果這個類不是抽象類,是否實現類父類或介面中要求實現的方法

4> 類中的欄位、方法是否和父類產生矛盾(例如覆蓋了父類的final欄位或者出現不符合規則的方法過載等)

3. 位元組碼驗證

4.  符號引用驗證

* 符號引用中的字串全限定名是否能找到對應的類

* 在指定類中是否存在符合方法的欄位描述以及簡單名稱鎖描述的方法和欄位

* 符號引用中的類、欄位、方法的訪問性是否可以被當前類訪問

準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念需要強調下.

首先,這個時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化隨著物件一起分配在java堆中

其次,設定類變數的初始值,指的是資料型別的零值,如果類欄位的欄位屬性表中存在ConstantValue屬性,那麼準備階段變數就會初始化為ConstantValue指定的值,比如

public static final int value = 123 ; 在準備階段value生成ConstantValue屬性,初始化為123而不是零


解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,符號引用指的是常量池中的常量(全類名,方法修飾符和方法名,欄位修飾符合欄位名),在class檔案中表示為表形式的常量,例如:CONSTANT_Fieldref_info

什麼是符號引用、直接引用?

符號引用:

符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用和虛擬機器實現的記憶體佈局無關,引用的目標不一定載入到了記憶體中,各種虛擬機器實現的記憶體佈局可以不同,但是能夠接受的符號引用必須一致,因為是定義在class檔案中

直接引用:

直接引用是能夠指向目標的指標,相對偏移量或是一個能間接定位到目標的控制程式碼。有了直接引用,那麼引用的目標必定存在記憶體中

重新回顧下,class檔案中常量池中的專案型別,有哪些


1. 類或介面的解析

假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或介面C的直接引用,虛擬機器整個解析需要3個步驟:

1> 如果C不是一個陣列型別,那麼虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C

2> 如果C是一個陣列型別,並且陣列的元素型別為物件,按照前面的規則載入陣列元素型別,接著有虛擬機器生成一個代表次陣列維度和元素的陣列物件

3> 如果上面的步驟沒有出現任何異常,那麼c在虛擬機器中實際上已經成為一個有效的類或介面了,但在解析完成前還要進行符號引用驗證,確認D是否具備對C的訪問許可權

2. 欄位解析

要解析一個未被解析過的欄位符號引用,首先將會對欄位表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,如果欄位所屬的介面或者類的符號引用解析異常,就會導致欄位解析失敗,欄位解析的步驟,當前類或介面使用C表示:

1> 如果C中包含簡單名稱和欄位描述符都和目標匹配的欄位,直接返回這個欄位的直接引用

2> 如果C中實現了介面,將會按照繼承關係從下到上遞迴搜尋各個介面和它的父介面,如果介面中欄位名和描述符和目標匹配,直接返回它的直接引用

3> 如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果父類中存在欄位名和描述符合目標匹配,直接返回它的直接引用

4> 否則,解析失敗

最後,如果查詢過程成功返回了引用,將會對這個欄位進行許可權驗證,如果不具備對欄位的訪問許可權,將丟擲異常。並且如果一個同名欄位同時出現在C的介面和父類中,或者在自己或父類的多個介面中出現,那麼編譯器將可能拒絕編譯

3.類方法的解析

首先對方法表中class_index項中索引CONSTANT_Methodref_info所屬的類或介面方法的符號引用進行解析, 如果解析成功,才會繼續,否則解析失敗,類方法解析的步驟如下(使用C表示這個類):

1> 類方法和介面方法符號引用的常量型別是分開的,如果在類方法表中發現class_index索引的C是個介面,解析失敗

2> 第一步校驗通過後,在C類中查詢是否有簡單名稱和描述符都和目標匹配的方法,如果有,返回這個方法的直接引用

3> 否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都和目標匹配的方法,如果有,返回這個方法的直接引用

4> 否則,在類C實現的介面列表以及它們的父介面中遞迴查詢是否有匹配到的,如果存在,說明C是一個抽象類,此時查詢結束,丟擲異常java.lang.AbstractMethodError

5> 否則,方法查詢失敗,丟擲java.lang.NoSuchMethodError

最後,如果查詢過程中返回了直接引用,將會對方法進行許可權驗證,如果不具有對此方法的訪問許可權,將丟擲異常

4. 介面方法解析

介面方法也需要解析出介面方法表的class_index索引表示的類或介面的符號引用,如果解析失敗,則方法解析失敗

,成功的話,繼續介面方法解析

1> 如果解析class_index出來的是類不是介面,直接就拋異常,解析失敗

2> 如果當前介面和父介面中有方法名、描述符和目標匹配的,直接返回它的直接引用

3> 否則,查詢失敗,丟擲java.lang.NoSuchMethodError異常

因為介面中的方法都是public ,不存在訪問許可權的問題,因此不丟擲java.lang.IllegalAccessError異常

初始化

類的初始化就是執行類構造器<clinit>()方法的過程。關於類執行構造器,有幾點需要注意:

* 類構造器方法指的是編譯器自動收集類中所有類變數的賦值動作和靜態語句塊static{}中的語句合併產生的。

(靜態語句塊只能訪問到定義在靜態語句塊前面的變數,定義在語句塊後面的變數可以賦值,但是不能訪問)

* <clinit>()方法與類的建構函式不同,它不需要顯式的呼叫父類構造器,虛擬機器執行時,能夠保證父類的<clinit>()必定在子類的<clinit>()方法執行前,就執行完畢,而且最先執行必定是Object的<clinit>()方法

* <clinit>()方法對於類或介面而言,並不是必須的,一個類沒有靜態語句塊,沒有類變數,就可以不生成<clinit>()方法

* 介面中不能使用靜態語句塊,但是介面中仍然存在變數的賦值操作,也會存在<clinit>()方法,只不過子介面執行<clinit>()方法不需要先去執行父介面的<clinit>()方法,只有父介面變數使用時,才會執行

多執行緒環境下,<clinit>()方法也是執行緒安全的。如果多個執行緒去初始化一個類,只會有一個執行緒執行<clinit>()方法,其他執行緒會阻塞,直到<clinit>()執行完畢

相關文章