代碼編譯的結果從本地機器碼轉換為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。
概述
虛擬機把描述一個類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機使用的Java類型,這就是虛擬機的類加載機制。
1、類加載的時機
類加載的整個生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載7個過程,其中驗證、準備和解析統稱為連接。
虛擬機沒有對什麼時候進行類的加載有強制約束,但是對於初始化階段,虛擬機規範則是嚴格規定了有且只有5中情況必須立即對類進行初始化(加載、驗證、準備和初始化自然得在初始化前完成):
遇到new、getstatic、putstatic和invokestatic這四條字節碼指令時,如果類沒有進行過初始化,則需要觸發其初始化(初始化自然存在類的加載)。這四條指令最常見的場景:使用new關鍵字實例化對象、獲取或設置一個類的靜態字段(被final修飾的除外)的時候和使用一個類的靜態方法時。
使用java.lang.reflect包的方法對類進行反射調用的時候,如果沒有對類進行過初始化,則觸發初始化。
當初始化一個子類時,發現其父類沒有初始化時,需先觸發父類的初始化。
當虛擬機啟動時,用戶需要指定一個要執行的主類(含有main方法的類)時,虛擬機會先初始化這個類。
當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
2、類加載的過程
加載
類加載階段,虛擬機需要完成以下3件事件:
通過一個類的全限定名獲取定義該類的二進制字節流
將字節流代表的靜態存儲結構轉換為方法區的運行時數據結構
在方法區中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
一個非數組類的加載,既可以使用虛擬機提供的引導類加載器來完成,也可以自定義類加載器完成(即重寫一個類加載器的loadClass方法)。對於數組類而言,情況有所不一樣,數組類本身不通過類加載器創建,而是由虛擬機自己創建。
驗證
驗證是連接階段的第一步,目的是保證Class文件的字節流包含的信息符合當前虛擬機的要求,保證輸入的字節流能正確被解析並存儲於方法區。驗證階段主要包括以下4個階段:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
文件格式驗證
第一階段驗證字節流是否符合Class文件的格式規範,並且能當前被虛擬機處理。這一階段主要驗證點:
是否以魔數開頭
版本是否在本機虛擬機處理範圍內
指向常量的各種索引值是否有指向不存在的常量
......
元數據驗證
第二階段主要是對類的元數據信息進行語義分析,保證不存在不符合Java語言規範的元數據。驗證點有:
這個類是否有父類
這個類是否繼承了不允許被繼承的類(被final修飾的類)
如果這個類不是抽象類,是否實現了其父類或繼承的接口要求實現的方法
......
字節碼驗證
第三個階段是驗證過程中最複雜的階段,主要目的是通過數據流和控制流分析程序語義是否合法。這個階段對類的方法體進行校驗,保證被校驗方法運行時不會危害虛擬機:
保證跳轉指令不會跳轉到方法體以外的字節碼指令
保證方法體中的類型轉換時是正確的
-
......
符號引用驗證
最後一個階段發生在虛擬機將符號引用轉換為直接引用的時候,這個轉換動作發生在連接的第三個階段 - 解析。符號引用可以看做是對常量池中各種符號引用進行校驗,驗證點有:
符號引用中通過字符串描述的全限定名是否找到對應的類或接口
符號引用中的類、字段、方法的訪問性是否可被當前類訪問
......
準備
準備階段是正式為類變量(被static修飾的變量,不包括實例變量)分配內存並設置類變量初始值的階段,這些內存在方法區進行分配。還有,這裡所說的初始值通常情況下是指數據類型的零值。
public static int value = 123
value變量在準備階段後的初始值為0,而不是123,因為這個時候並未開始執行任何java方法,而把value賦值為123的putstatic指令時程序被編譯後,存放於類構造器方法中的,所以把value賦值為123的操作是在初始化階段才執行的。
上面提到,通常情況下是數據類型的零值,但是有一些特殊情況就不一樣:如果類變量被final修飾,在準備階段 ,類變量就會被初始化為指定的值。
public static final int value = 123;
在準備階段,value的值就會被賦值為123.
解析
解析階段就是虛擬機將常量池內的符號引用替換為直接引用的過程。虛擬機規範並未對什麼時候進行解析階段有規定,只要求了**在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokeestatic、involevirtual、ldc、ldc_w、multianewarray、new、putstatic和putfield這16個用於操作符號引用的字節碼指令之前,先對他們所使用的符號引用進行解析 **。所以虛擬機可以根據需要來判斷是在類被加載器加載時就對符號引用進行解析或是在符號引用在被使用前才去解析。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行解析。
類或接口的解析
假設當前代碼所處的類為D,如果想把一個從未解析過的符號引用N解析到一個類或接口C的直接引用,虛擬機完成整個解析階段的過程分為以下3步:
如果C是不是一個數組類型,虛擬機將會把符號引用N的全項定類名傳遞給D的類加載器去加載這個類C。在加載的過程中由於需要驗證,可能又會觸發其他類的加載,一當加載過程出現錯誤,解析過程直接失敗。
如果C是一個數組類型,數組元素也是對象類型的話,N的描述符將會是類似[Ljava/lang/Integer的形式。那將會按照第一點的規則加載數組元素類型,接著由虛擬機生成一個代表此數組維度和元素的數組對象
-
如果前面的步驟都沒有出現錯誤,在解析完成前還需要進行符號引用的驗證,確認D是否具備對C的訪問權限,如果D沒有對C的訪問權限,拋出java.lang.IllegalAccessEroor異常。
字段解析
要解析一個未被解析過的字段的符號引用。首先會對字段表內的class_index項索引的CONSTANT_Class_info符號引用解析,也就是字段所屬的類或接口的符號引用。如果在解析這個類或接口的符號引用出現異常,都會導致字段解析的失敗。如果這個類或接口解析成功,將對這個字段所屬的類或接口用C表示,然後對C進行後續的字段搜索:
如果C本身就包含了簡單名稱和字段描述符都與目標字段相同的字段,則返回這個字段的 直接引用,查找結束
否則,如果在C中實現了接口,將會按照繼承關係從下往上遞歸搜索每個接口和它的父接口,然後按照步驟1去查找
否則,如果C不是object類的話,按照繼承關係從下往上遞歸搜索其父類,然後按照步驟1去查找
否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
類方法解析
類方法的解析的第一個步驟與字段解析一樣,也需要先解析出類方法表的claaa_index索引的方法所屬類或接口的符號引用,如果解析成功,用C表示這個類,接下來虛擬機按照以下步驟進行類方法的搜索:
在類C中查找是否有簡單名稱和描述符都與目標匹配的方法,如果有返回這個方法的直接引用,查找結束
否則在類C的父類中遞歸查找
否則在類C的接口或父接口中查找
否則查找失敗,拋出java.lang.NoSuchMethodError異常。
接口方法解析
接口方法解析與類方法解析類方法解析類似,這裡不再冗餘。
初始化
初始化階段是類加載過程的最後一步。在前面的類加載過程中,除了在加載階段可以自動定義加載器參與類的加載過程外,其餘的動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java代碼。
在準備階段,變量已經被賦值為系統要求的零值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。或者說初始化階段是執行類構造器方法的過程。
方法是由編譯器自動收集類中的所有類變量的複製操作和靜態語句塊(static{})中的所有語句合併而生的。靜態語句塊只能訪問到定義在靜態語句前的變量,定義在它之後的變量,只能在靜態語句塊中賦值而不能訪問。
方法與類的構造函數不同,它不需要顯示地調用父類構造器,虛擬機會保證在子類的方法執行前執行父類的方法。
方法並不是必須的,如果一個類沒有靜態語句塊,也沒有變量的賦值操作,編譯器可以不為這個類生成方法。
虛擬機會保證一個類的方法在多線程環境下被正確地加鎖、同步,如果多線程同時去初始化一個類,只會有一個線程執行方法。
類與類加載器
虛擬機設計團隊把類加載階段的通過一個類的全限定名獲取此類的二進制字節流這個動作放到Java虛擬機外部實現,以便讓開發人員自己決定如何獲取所需要的類,實現這個動作的代碼模塊稱為“類加載器”。
對於任意一個類,都需要加載它的類加載器和這個類本身一同確定其所在虛擬機的唯一性。通俗地說,比較兩個類是否相等,只有在相同的類加載器的前提下才有意義,否則,即使這兩個類來自於同一個Class文件,被同一個虛擬機加載,只要類加載器不一樣,這兩個類就不可能相等。
雙親委派模型
從虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器,是虛擬機的一部分;另一種是其他的類加載器,獨立於虛擬機之外,而且全都繼承於抽象類java.lang.ClassLoader.
從開發人員的角度來看,絕大部分java程序都會使用到以下3種系統提供的類加載器:
啟動類加載器
這個類負責將放在
\lib目錄下的並且被虛擬機識別的(按照文件名識別,名字不符合的類庫即使放在lib目錄下也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被java程序直接引用。 拓展類加載器
它負責加載
\lib\ext目錄下的所有類庫,開發者可以直接使用拓展類加載器 -
引用程序類加載器
它負責加載用戶類路徑(ClassPath)下所指定的類庫,開發者可以直接使用。如果程序中沒有自定義自己的類加載器,一般情況下這個就是程序默認的類加載器。
雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應該有自己的父類加載器、但是這裡的類加載器之間的父子關係不是以繼承關係實現的,而是使用組合關係來複用父類加載器。
雙親委派模型的工作流程:
如果一個類加載器收到了一個類加載請求,它不會自己去加載這個類,而是將請求委派給它的父類加載器去加載,每一個層次的類加載器都是這樣,因此所有的類加載請求最終都會落到頂層的啟動類加載器,只有當父類加載器五法加載這個請求時(它的搜索範圍中沒有找到所需的類),子加載器才會嘗試自己去加載。使用雙親委派的好處就是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關係。
關注“編碼之道”,及時查看最新內容……
閱讀更多 編碼之道 的文章