java虛擬機器類載入機制淺談

NO IMAGE

 Java語言是一種編譯後再經過直譯器執行的過程, 直譯器主要就是如何處理解釋Class檔案的二進位制位元組流。JVM主要包含三大核心部分:執行時資料區,類載入器和執行引擎。

       虛擬機器將描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、準備、解析和初始化,最終就會形成可以被虛擬機器使用的Java型別,這就是一個虛擬機器的類載入機制。Java中的類是動態載入的,只有在執行期間使用到該類的時候,才會將該類載入到記憶體中,Java依賴於執行期動態載入和動態連結來實現類的動態使用。

一個類的整個生命週期如下:

    載入,驗證,準備,初始化和解除安裝在開始的順序上是固定的,但是可以交叉進行。

   在Java中,對於類有且僅有四種情況會對類進行“初始化”。

     1) 使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態欄位時候(除final修飾的static外),呼叫類的靜態方法時候,都只會初始化該靜態欄位或者靜態方法所定義的類。

     2) 使用reflect包對類進行放射呼叫的時候,如果類沒有進行初始化,則先要初始化該類

     3) 當初始化一個類的時候,如果其父類沒有初始化過,則先要觸發其父類初始化。

     4)  虛擬機器啟動的時候,會初始化一個有main方法的主類。

注意:通過子類引用父類靜態欄位,只會初始化父類不會初始化子類;通過陣列定義來引用類,也不會觸發該類的初始化;常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此也不會觸發定義常量的類的初始化。

一 類載入過程

1 載入

       載入階段主要完成三件事,即通過一個類的全限定名來獲取定義此類的二進位制位元組流,將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構,在Java堆中生成一個代表此類的Class物件,作為訪問方法區這些資料的入口。這個載入過程主要就是靠類載入器實現的,這個過程可以由使用者自定義類的載入過程。

2 驗證

這個階段目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,不會危害虛擬機器自身安全。主要包括四種驗證:

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

     後設資料驗證:基於方法區的儲存結構驗證,對位元組碼描述資訊進行語義驗證。

     位元組碼驗證:基於方法區的儲存結構驗證,進行資料流和控制流的驗證。

     符號引用驗證:基於方法區的儲存結構驗證,發生在解析中,是否可以將符號引用成功解析為直接引用。

3 準備

僅僅為類變數(即static修飾的欄位變數)分配記憶體並且設定該類變數的初始值即零值,這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,同時這裡也不會為例項變數分配初始化。類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中。

4 解析

解析主要就是將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼。有類或介面的解析,欄位解析,類方法解析,介面方法解析。

 這裡要注意如果有一個同名欄位同時出現在一個類的介面和父類中,那麼編譯器一般都會拒絕編譯。

5 初始化

    初始化階段依舊是初始化類變數和其他資源,這裡將執行使用者的static欄位和靜態語句塊的賦值操作。這個過程就是執行類構造器<clinit>方法的過程。

<clinit>方法是由編譯器收集類中所有類變數的賦值動作和靜態語句塊的語句生成的,類構造器<clinit>方法與例項構造器<init>方法不同,這裡面不用顯示的呼叫父類的<clinit>方法,父類的<clinit>方法會自動先執行於子類的<clinit>方法。即父類定義的靜態語句塊和靜態欄位都要優先子類的變數賦值操作。

二 類載入器

1   類載入器的分類

       啟動類載入器(Bootstrap ClassLoader):  主要負責載入<JAVA_HOME>\lib目錄中的,或是-Xbootclasspath引數指定的路徑中的,並且可以被虛擬機器識別(僅僅按照檔名識別的)的類庫到虛擬機器記憶體中。它載入的是System.getProperty(“sun.boot.class.path”)所指定的路徑或jar。

       擴充套件類載入器(Extension ClassLoader):主要負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。它載入的是

System.getProperty(“java.ext.dirs”)所指定的路徑或jar。

       應用程式類載入器(Application ClassLoader):也叫系統類載入器,主要負責載入ClassPath路徑上的類庫,如果應用程式沒有自定義自己類載入器,則這個就是預設的類載入器。

它載入的是System.getProperty(“java.class.path”)所指定的路徑或jar。

2   類載入器的特點

1)執行一個程式時,總是由Application Loader(系統類載入器)開始載入指定的類。

2)在載入類時,每個類載入器會將載入任務上交給其父,如果其父找不到,再由自己去載入。

3)Bootstrap Loader(啟動類載入器)是最頂級的類載入器了,其父載入器為null.

3   類載入器的雙親委派模型

       類載入器雙親委派模型的工作過程是:如果一個類載入器收到一個類載入的請求,它首先將這個請求委派給父類載入器去完成,每一個層次類載入器都是如此,則所有的類載入請求都會傳送到頂層的啟動類載入器,只有父載入器無法完成這個載入請求(即它的搜尋範圍中沒有找到所要的類),子類才嘗試載入。

      使用雙親委派模型主要是兩個原因:1)可以避免重複載入,當父類已經載入了,則就子類不需再次載入;2)安全因素,如果不用這種,則使用者可以隨意的自定義載入器來替代Java核心API,則就會帶來安全隱患。

       下面是一個類載入器雙親委派模型,這裡各個類載入器並不是繼承關係,它們利用組合實現的父類與子類關係。

4 類載入的幾種方式

1) 命令列啟動應用時候由JVM初始化載入,載入含有main的主類。

      2)通過Class.forName(“Hello”)方法動態載入類,預設會執行初始化塊,這是因為Class.forName(“Hello”)其實就是Class.forName(“Hello”,true,CALLCLASS.getClassLoader()),第二個引數就是類載入過程中的連線操作。如果指定了ClassLoader,則不會執行初始化塊。

      3)通過ClassLoader.loadClass(“Hello”)方法動態載入類,不會執行初始化塊,因為loadClass方法有兩個引數,使用者只是用第一個引數,第二個引數預設為false,即不對該類進行解析則就不會初始化。

5 類載入例項

當在命令列下執行:java HelloWorld(HelloWorld是含有main方法的類的Class檔案),JVM會將HelloWorld.class載入到記憶體中,並在堆中形成一個Class的物件HelloWorld.class。

      基本的載入流程如下:

      1)尋找jre目錄,尋找jvm.dll,並初始化JVM;

      2)產生一個Bootstrap Loader(啟動類載入器);

      3)Bootstrap
Loader,該載入器會載入它指定路徑下的Java核心API,並且再自動載入Extended Loader(標準擴充套件類載入器),Extended Loader會載入指定路徑下的擴充套件JavaAPI,並將其父Loader設為BootstrapLoader。

      4)Bootstrap
Loader也會同時自動載入AppClass Loader(系統類載入器),並將其父Loader設為ExtendedLoader。

     5)最後由AppClass Loader載入CLASSPATH目錄下定義的類,HelloWorld類。

本文出自 “在雲端的追夢” 部落格,請務必保留此出處http://computerdragon.blog.51cto.com/6235984/1223354