Java虛擬機類加載機制

Java程序運行於Java虛擬機之上,JVM屏蔽了底層細節,使得Java程序能夠“一次編譯,到處運行”。在Java語言中,一切皆是對象,代碼一般由類、接口、enum等構成,是一種面向對象的編程語言。本文將為你揭示Java虛擬機如何加載類,一窺Java底層的祕密。

類在虛擬機中的生命週期,可以分為加載、驗證、準備、解析、初始化、使用、卸載幾個階段,其中的驗證、準備、解析統稱為連接。在這裡,讀者可以回憶一下以C語言為代表的面向過程語言如何實現動態鏈接庫,以更好地理解Java面向對象編程。

Java虛擬機類加載機制

通常情況下,虛擬機都會按照上圖流程管理類的生命週期。然而,Java語言的一大特性——多態支持方法的動態綁定,即,調用方法前無法知道具體調用了那個方法,只有運行到調用的時刻才能確定方法的具體實現。因此,解析也可能發生在初始化之後,在多態調用時才解析出具體的直接引用。

加載

在Java虛擬機規範中,並沒有強制要求什麼時候加載類,由虛擬機自行把握。在加載階段,虛擬機通過一個類的全限定名獲取類的二級制字節流,把字節流的靜態存儲結構轉換為運行時數據結構,在內存中生成一個Class對象,Class對象將作為方法區的訪問入口。

在Java中,能夠根據全限定名獲取字節流的代碼塊被稱為類加載器。主要包括啟動類加載器、擴展類加載器、應用程序類加載器和用戶自定義類加載器。其中,

啟動類加載器加載jre的lib目錄下的類,如rt.jar,在Hotspot虛擬機中用c++實現,是虛擬機的一部分;

擴展類加載器加載jre的lib/ext或者由系統變量 java.ext.dir指定目錄中的類,一般Java語言實現;

應用程序類加載器加載CLASSPATH中的類,一般Java語言實現;

自定義類加載器用於程序實現個性化的類加載,如spring提供的ClassLoader、用於熱升級的ClassLoader、從網絡加載jar包的ClassLoader。

在加載類的過程中,Java採用了雙親委派機制。而這種父子關係並不是通過繼承實現的,而是組合關係。一個類加載器需要加載類時,首先委託父類加載器進行加載,並逐級向上,如果父類加載器加載成功則返回成功,如果父類加載器加載失敗,則自己進行加載。在Java中,類的唯一性是由類和所屬的類加載器共同確定的。兩個類加載器加載的同一個class,在虛擬機看來也是不同的類。通過雙親委派機制,Java能夠保證核心類不會被用戶覆蓋,因用戶企圖覆蓋核心類時類加載器總能找到已由父類加載器加載的核心類。

Java虛擬機類加載機制

驗證

只要符合class文件的格式要求的class文件都能被虛擬機加載,不管class文件是不是由Java編譯器所產生。Java虛擬機出於自身安全的考慮,會對加載的類進行合法性驗證。

在驗證階段,虛擬機將進行文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。此階段主要的目標是確保Class文件的字節流包含的信息符合虛擬機的要求。

文件格式驗證:驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。如是否以魔數0xCAFEBABE開頭,主、次版本號是否在當前虛擬機處理範圍之內等。

元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,如是否有父類,父類是否繼承了不允許被繼承的類,類中的字段、方法是否與父類產生矛盾等。

字節碼驗證:對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。如保證跳轉指令不會跳轉到方法體以外的字節碼指令上。

符號引用驗證:對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,如符號引用中通過字符串描述的全限定名是否能找到對應的類,符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問等。

準備

在準備階段,虛擬機將為類變量在方法區分配內存並設置類變量的初始值。此時的初始值並不是源代碼中的初始值,而是各種類型變量的默認初始值,如int類型為0、boolean類型為false。源代碼中的變量初始值,會在<clinit>方法中賦值,在初始化階段完成。

解析

在解析階段,虛擬機將常量池內的符號引用替換為直接引用。在類初始化之前,解析操作只能解析靜態綁定的符號引用。

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

直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

初始化

虛擬機規範要求有且只有以下幾種情況觸發類的初始化,也就是說連接之後的類並沒有立即執行初始化,而是在使用前才進行初始化:

①使用new創建對象、讀寫類的靜態字段、調用類的靜態方法時需要進行初始化,但final修飾的字段除外。讀寫靜態字段時,只有靜態字段所在的類會被初始化。

②使用反射調用的時候,如果類沒有初始化則先初始化。

③初始化一個類的時候,如果它的父類還沒有初始化,則先觸發其父類的初始化。

④虛擬機啟動時,用於執行的包含main方法的類需要先初始化;

⑤使用動態語言支持時,如果解析結果引用的類沒有進行初始化,則需要先初始化。

在編譯階段,編譯器會掃描源文件,根據類中的變量賦值和靜態語句塊生成<clinit>方法,並在初始化階段執行<clinit>方法。

<clinit>方法中初始化過程與源代碼中語句順序保持一致,靜態語句塊只能訪問之前的變量,對於之後的變量只能賦值不能訪問。如果類中沒有變量賦值和靜態語句塊,則不會生成<clinit>方法。在講解繼承的時候,通常都會提到父類會先於子類進行初始化,一定程度上也是因為父類的<clinit>方法會先於子類執行。

如果接口定義了常量,也會生成<clinit>方法,與類不同的是,接口初始化時不需要先調用父接口的<clinit>方法,只有在用到父接口的變量時才執行父接口的<clinit>方法。並且,接口的實現類在初始化時也不會調用接口的<clinit>方法,因此方法屬於接口不屬於實現類。

在初始化過程中,虛擬機會保證多線程併發情況下類能夠被正確初始化,即<clinit>方法會被虛擬機加鎖和同步,同一時間只有一個線程能夠執行<clinit>方法。

總結

虛擬機的類加載機制,分為加載、驗證、準備、解析、初始化五個階段。採用雙親委派機制從類的文件二進制流載入,然後進行類的合法性驗證,分配類的運行時數據空間,對符號引用進行解析轉為直接引用,最後執行類的初始化。

相關推薦

推薦中...