1. 程式人生 > >深入理解Java虛擬機器學習筆記——三、虛擬機器類載入機制

深入理解Java虛擬機器學習筆記——三、虛擬機器類載入機制

1、概述

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成能夠被虛擬機器直接使用的資料型別,這就是虛擬機器的類載入機制。在Java中,類的載入、校驗、解析和初始化都是在執行期間完成的。

2、類載入的時機

類從被載入都虛擬機器記憶體開始,到卸載出記憶體為止,它的整個生命週期包括:載入、準備、校驗、解析、初始化、使用和解除安裝7個階段。準備、校驗、解析被統稱為連線。
其中,載入、驗證、準備、初始化和解除安裝這的順序是確定的,類的載入過程必須按照這種順序按部就班的開始。這裡的開始並不是按部就班的進行或者完成,因為這些階段都是相互交叉混合的進行的,通常會在一個階段執行的過程中呼叫、啟用其他階段。 虛擬機器規範嚴格的規定了有且只有5中情況必須立即對類進行初始化操作: 1)遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化操作。生成這4條位元組碼指令的場景:使用new關鍵字例項化物件時、獲取或設定一個類的靜態欄位時(被final修飾,在編譯期把結果放入常量池的靜態欄位除外)以及呼叫一個類的靜態方法時。 2)呼叫java.lang.reflect包中的方法對類進行反射呼叫時,如果類還沒有初始化,則先觸發其初始化操作。 3)當初始化一個類時,如果發現其父類還沒有初始化,則先觸發其父類的初始化操作。 4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的類),虛擬機器會先初始化這個主類。 5)當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

3、類載入過程

(1)載入

“載入”是“類載入”(Class Loading)過程的一個階段。在載入階段,虛擬機器需要完成以下3件事: ① 通過一個類的全限定名來獲取定義此類的二進位制位元組流。 ② 將這個位元組流所代表的靜態儲存結構轉換成方法區的執行時資料結構。 ③ 在記憶體中生成一個代表該類的java.lang.Class物件,作為方法區該類的各種資料訪問入口。 獲取二進位制位元組流的方式主要有:
  • 從ZIP包中獲取,例如:JAR、WAR、EAR等。
  • 從網路中獲取,最典型的場景是Applet。
  • 執行時計算生成,使用最多的場景是動態代理技術,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來為特定介面生成形式為“*$Proxy”的代理類的二進位制位元組流。
  • 由其他檔案生成,典型場景是JSP應用,即由JSP檔案生成對應的Class類。
  • 從資料庫中讀取,例如有些中介軟體(SAP Netweaver)可以選擇把程式安裝到資料庫中來完成程式程式碼在資料庫中的分發。
相對於類載入過程中的其他階段而言,非陣列類的載入階段是開發人員可控性最強的,因為類載入階段既可以使用系統提供的引導類載入器來完成,也可以使用使用者自定義的類載入器完成。開發人員可以定義自己的類載入器來控制二進位制位元組流的讀取方式,通過覆寫loadClass()方法。 陣列則不一樣,陣列的建立是由虛擬機器來完成的。但是陣列依然與類載入其有著緊密的關係,因為陣列的元素型別(即去掉所有維度的型別)最終還是由類載入器建立的。一個數組類(以下簡稱為C)的建立遵循以下規則:
  • 如果陣列的元件型別(Component Type,即陣列去掉一個維度後的型別)是引用型別,則遞迴呼叫之前定義的類載入過程去載入這個元件型別,陣列C將在載入該元件型別的類載入器的名稱空間上被標識。
  • 如果陣列的元件型別不是引用型別(如int[]陣列),Java虛擬機器將會把陣列標記為引導類載入器關聯。
  • 陣列類的可見性與他的元件型別的可見性一致,如果元件型別不是引用型別,那麼陣列類的可見性預設為public。
載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中。然後在記憶體中例項化一個java.lang.Class物件(並沒有明確規定放在Java堆中,在Hotsopt虛擬機器中,Class物件比較特殊,雖然是物件,但是存放在方法區中),這個物件將作為方法區中訪問這些型別資料的外部介面。 載入階段與連線階段的部分內容是交叉進行的,載入階段還沒完成,連線階段可能已經開始了。但是夾雜在載入階段的動作仍然屬於連線階段,這兩個階段的開始時間依然保持至固定的先後順序。

(2)驗證

驗證是連線階段的第一步,該階段的目的是確保Class檔案的位元組流中所包含的資訊符合當前虛擬機器的要求,並且不會危害到虛擬機器自身的安全。之前說過Class檔案並不一定是由Java編譯而來的,虛擬機器如果不檢查輸入的位元組流,很可能因為載入了有害的位元組流而導致系統崩潰,所以,驗證是虛擬機器保護自身的一項重要工作。 驗證階段大致完成以下4個階段的校驗工作: ① 檔案格式驗證 第一階段要驗證位元組流是否符合Class檔案格式的規範,是否能被當前版本的虛擬機器處理。這一階段可能包括以下驗證點:
  • 是否以魔數0xCAFEBABE開頭。
  • 主、次版本號是否在當前版本虛擬機器的處理範圍之內。
  • 常量池中的常量是否有不被支援的常量型別(檢查常量的tag標誌)。
  • 指向常量的各種索引值中是否有隻要不存在的常量或不符合型別的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合utf8編碼的資料。
  • Class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊。
以上的幾種檔案格式驗證只是該驗證階段的一小部分而已,該階段的主要目的是保證輸入的位元組流能夠被正確地解析並存儲於方法區,格式上符合描述一個Java型別資訊的要求。這個階段的驗證是基於二進位制位元組流的,只有通過了該階段的驗證後位元組流才會被儲存在記憶體的方法區中。後面3個階段的驗證都是基於方法區的儲存結構進行的,不會再操作位元組流。 ② 元資料驗證 對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求,該階段可能包含的驗證:
  • 這個類是否父類(除java.lang.Object外,所有的類都應該有父類)。
  • 這個類的父類是否繼承了不允許被繼承的類(被final所修飾的類)。
  • 如果這個類不是父類,是否實現了其父類或介面中要求實現的所有方法。
  • 類中的欄位或方法是否與父類產生矛盾(覆蓋了被final修飾的欄位,或者出現不符合規則的方法過載,例如方法引數都一致,但返回值型別卻不相同等)。
③ 位元組碼驗證 通過對資料流和控制流分析,確保程式語義是合法的,符合邏輯的。在元資料校驗階段對元資料中的資訊校驗完成後,這個階段將對方法體進行校驗,以保證類的方法在執行時不會發生危害到虛擬機器事件,例如:
  • 保證任意時刻運算元棧的資料型別都能與指令程式碼序列配合工作,例如不會出現在運算元棧放了一個int型別的資料,使用時卻按long型別來載入進本地變量表中。
  • 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
  • 保證方法體中的型別轉換是有效的。
④ 符號引用驗證 該階段的驗證發生在虛擬機器將符號引用轉換成直接引用的時候,這個轉換的動作將在連線的第三階段——解析階段發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,通常需要校驗一下內容:
  • 符號引用中通過字串描述的全限定名是否能夠找到對應的類。
  • 在指定的類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位。
  • 符號引用中的類、方法、欄位的訪問性(private、protected、default、public)是否可被當前類訪問。
符號引用驗證的目的是為了確保解析動作的正常執行

(3)準備

準備階段是正式為類變數分配記憶體並設定初始值的階段,這些變數所使用的記憶體都將在方法區中分配。在這一階段進行記憶體分配的僅僅是指類變數,即被static修飾的變數。而例項變數將會在物件例項化時隨著物件一起在Java堆中分配記憶體。“初始值”通常情況下是資料型別的零值,而真正的賦值是發生在初始化階段的。下圖羅列了Java中所有基本資料型別的零值:
除了“通常情況”以外的“特殊情況”:如果類欄位的欄位屬性表中存在ConstantValue屬性,那麼在準備階段,類變數就會被初始化為ConstantValue屬性所指定的值,如public staticfinalint value = 123;

(4)解析

解析階段是虛擬機器將常量池中的符號引用替換為直接引用的過程。在解析階段中,符號引用與直接引用的關聯:
  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用目標,符號可以是任意形式的字面常量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指標、相對偏移量或是一個能夠間接定位到目標的控制代碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器實現上翻譯出來的直接引用一般不會相同。
虛擬機器規範中並未規定解析階段發生的具體時間,只要求在執行了anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokestatic、invokeinterface、invokespecial、invokevirtual、ldc、ldc_w、multianewarray、putfield和putstatic這16個用於操作符號引用的位元組碼指令之前,先對它們所使用的符號引用進行解析。所以虛擬機器可以根據實際需要來判斷是在類被載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用在將要被使用前解析。 對同一個符號引用進行多次解析請求是很常見的,出來invokedynamic指令以外,虛擬機器實現可以對第一次解析的結果進行快取(在執行時常量池中記錄直接引用,並將常量標識為已解析狀態),從而避免重複解析。無論是否進行了多次解析,虛擬機器需要保證在同一個實體中,如果一個符號引用之前已經被成功解析過,那麼後續的解析請求就應當一直成功;反之,如果第一次解析失敗了,那麼其他指令對這個符號引用的解析請求也應當收到相同的異常。 對於invokedynamic指令,當碰到前面某個已經由invoke觸發過解析的符號引用時,並不意味著這個解析結果對其他的invokedynamic指令同樣生效。因為invokedynamic指令的目的本來就是用於動態語言支援的,它所對應的引用被稱為“動態呼叫點限定符”。 解析動作主要用於類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7中符號引用,分別對應於常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info等7中常量型別。

(5)初始化

初始化階段是類載入過程的最後一步,在這一階段才真正開始執行類中定義的Java程式碼。在準備階段,類變數已經賦值過一次系統要求的初始值,而在初始化階段則是根據程式設計師通過程式制定的主觀計劃去初始化類變數及其它資源。

4、類載入器

實現“通過一個類的全限定名來獲取此類的二進位制位元組流”的動作的程式碼模組被稱為類載入器。

(1)類與類載入器

對於任意一個類,都需要通過載入該類的類載入器與該類本身一同來確定其在Java虛擬機器中的唯一性,每一個類載入器都擁有一個獨立的類名稱空間。也就是說,比較兩個類是否“相等”,只有在這兩個類都是由同一個類載入器載入的前提下才有意義。這裡所指的“相等”,包括Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結構,也包括使用instanceof關鍵字做物件所有關係判定等情況。

(2)雙親委派模型

從Java虛擬機器角度來看,只存在兩種類載入器:① 啟動類載入器(Bootstrap ClassLoader),這個類載入器由C++實現,是虛擬機器的一部分。② 所有其它的類載入器,這些類載入器由Java語言實現,獨立於虛擬機器外部,並且全部都是ClassLoader的子類。 從Java開發人員角度來看,類載入器可以分為: ① 啟動類載入器(Bootstrap ClassLoader):這個類載入器負載將<JAVA_HOME>\lib目錄中或被-Xbootclasspath引數所指定的路徑中的,並且是被虛擬機器識別的(僅按照檔名識別,如:rt.jar)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式設計師直接引用,如果需要把載入請求委派給引導類載入器,那麼直接使用null代替即可。 ② 擴充套件類載入器(Extension ClassLoader):這個載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。 ③ 應用程式類載入器(Application ClassLoader):這個載入器由sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被稱為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過類載入器,那麼這個就是程式中預設的類載入器。 類載入器之間的層次結構:
上圖所展示的層次結構被稱為雙親委派模型。雙親委派模型要求,除了頂層的啟動類載入器外,所有的類載入器都應當有自己的父類載入。這裡類載入器之間的父子關係是通過組合關係實現的。 雙親委派模型的工作過程:如果一個類載入器收到了類載入請求,它首先並不會自己去載入這個類,而是把這個請求委派給父載入器去完成,每一個層次的類載入器都是如此,因此所有的類載入請求最後都會傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求時(它的搜尋範圍中沒有找到所需的類),子載入器才會嘗試自己去載入。

相關推薦

no