1. 程式人生 > >Java虛擬機器:類載入機制詳解

Java虛擬機器:類載入機制詳解

    大家知道,我們的Java程式被編譯器編譯成class檔案,在class檔案中描述的各種資訊,最終都需要載入到虛擬機器記憶體才能執行和使用,那麼虛擬機器是如何載入這些class檔案的呢?在載入class檔案的過程中虛擬機器又幹了哪些事呢?今天我們來解密虛擬機器的類載入機制。

       虛擬機器把class檔案載入到記憶體,並對資料進行校驗、解析和初始化,最終形成可以被虛擬機器直接使用的Java型別(Class物件),這就是虛擬機器的類載入機制。

       類從被載入到虛擬機器記憶體開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。,其中驗證、準備和解析3個階段統稱為連線階段。如圖:

       

       前面的5個階段就是類載入的過程。其中載入、驗證、準備和初始化這幾個階段的順序是確定的,而解析階段則不一定,在某些情況下它可以在初始化階段以後才進行。那麼,在類載入的每一個步驟中,虛擬機器都進行了那些工作呢?

       載入

      載入是類載入過程的第一個階段,在這一階段,虛擬機器主要完成了3件事:

       1、通過類的全限定名來獲取定義這個類的二進位制位元組流。簡單來說就是,通過類的包名加類名來定位到此類的class檔案的位置,相當於一個資源定位的過程。

       2、將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構。也就是將類中定義的靜態變數、常量等資訊儲存在方法區中。

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

       總結一下,載入階段的主要工作就是,把class二進位制檔案載入到記憶體後,將類中定義的靜態變數、常量、類資訊等資料存放到方法區,並在堆記憶體中建立一個代表這個類的Class物件,作為方法區中這個類的資料資訊的訪問入口,程式猿可以持有這個Class物件。

       驗證

       驗證是連線階段的第一步,驗證階段的目的是確保class檔案中包含的資訊符合虛擬機器的要求,並且不會危害到虛擬機器自身的安全。驗證的內容主要包含以下幾個方面:

       1、檔案格式驗證。主要目的是保證輸入的位元組流能正確的解析並存儲在方法區中,格式上符合一個Java型別資訊的要求。這個階段的驗證是基於二進位制位元組流進行的,只有通過了這個階段的驗證,位元組流才能進入方法區進行儲存,所有後面的3個階段的驗證都是基於方法區的儲存結構進行的,不會直接操作位元組流。

       2、元資料驗證。這一階段的主要目的是對類的元資料(定義資料的資料)資訊進行語義校驗,確保不存在不符合Java語言規範的元資料資訊。包括:該類是否有父類、該類的父類是否繼承了不允許被繼承的類、該類中的欄位和方法是否與父類產生矛盾等等。

       3、位元組碼驗證。目的是通過資料流和控制流分析,確定程式語義是否合法、符合邏輯。在第二階段對元資料資訊的資料型別做完校驗後,這個階段將對類的方法體進行分析,保證被校驗的類的方法在執行時不會危害虛擬機器的安全。

       4、符號引用驗證。符號引用驗證發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作是在連線的第三階段——解析階段中進行的。符號引用驗證的目的是確保解析動作能夠正常執行。

       對於類載入機制而言,驗證階段是一個非常重要、但不是一定必要的階段。如果所執行的全部程式碼都已經被反覆使用和驗證過了,那麼就可以使用虛擬機器引數-Xverify:none來關閉大部分的類驗證措施,以縮短類載入的時間。

       準備

       準備階段的主要工作是為類的靜態變數分配記憶體並設定變數的初始預設值。這些變數所使用的記憶體都在方法區中分配。這裡有兩個問題需要說明:

       1、這一階段進行記憶體分配的僅包括靜態變數,而不包括例項變數(靜態變數是所有物件共有的,例項變數是物件私有的),例項變數將會在物件例項化時隨著物件一起分配在Java堆中。

       2、這裡說的為物件賦初始值是各資料型別對應的零值。假設有一個靜態變數定義為public static int a = 1; 那變數a的初始值就是0而不是1,初始值1在初始化階段賦給變數a。如果是引用型別初始預設值就是null。

       解析

       解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

      符號引用:符號引用以一組符號來描述所引用的目標,符號可以使任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用的字面量形式在Java虛擬機器規範的Class檔案格式中有明確定義。

      直接引用:直接引用可以是直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局有關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。

      解析動作主要針對類或介面、欄位、靜態方法、介面方法、方法型別、方法控制代碼和呼叫點限定符這幾類符號引用進行。

      初始化

      初始化階段是類載入過程的最後一步。在前面的類載入過程中,除了載入階段我們可以通過自定義的類載入器參與之外,其餘的階段都是虛擬機器自動完成的。到了初始化階段,才真正開始執行我們程式中定義的Java程式碼。初始化階段的主要工作是給類的靜態變數賦予我們程式中指定的初始值。也就是上面準備階段提到的,變數a的值從0變為1的過程。這個階段我們程式指定初始值包括兩種手段:

      1、宣告靜態變數時顯式的複製。例如:public static int a = 1; 在初始化階段會把1賦給變數a。

      2、通過靜態程式碼塊賦值。例如:static { a = 2 }; 變數a 的初始值賦為2。

      這兩種方式的賦值順序是由語句在原始檔中出現的順序來決定的。 

      以上就是Java虛擬機器類載入機制的整個過程以及在每個階段虛擬機器所執行的動作。

      雙親委派模型

      前面提到過,在類載入的整個過程中,除了載入階段我們可以通過自定義的類載入器參與之外,其他的階段都是虛擬機器幫我們完成的。虛擬機器設計團隊把載入這個動作放到Java虛擬機器外部去實現,實現這個動作的程式碼模組稱為“類載入器”。這樣做的目的是讓應用程式自己去決定如何獲取所需要的類。

      除了我們自己可以定義類載入器,Java虛擬機器也為我們提供了系統自帶的類載入器。主要可以分為以下三種:

      根類載入器(Bootstrap ClassLoader):這個類載入器負責載入存放在<JAVA_HOME>\lib目錄中的,或者通過引數-Xbootclasspath所指定的路徑中的類。

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

      應用類載入器(Application ClassLoader):它負責載入使用者設定的ClassPath路徑上所指定的類庫。如果應用程式中沒有自定義的類載入器,一般情況下這個就是程式預設的類載入器。

      我們的應用程式都是由這3種類載入器相互配合進行載入的,如果有必要,還可以定義自己的類載入器。這些類載入器之間的關係如下:

      

       上圖中展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型。雙親委派模型要求除了頂層的根類載入器外,其餘的類載入器都應當有自己的父類載入器。雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的類載入請求最終都應該傳送到根類載入器中,只有當父載入器反饋自己無法完成這個載入請求(搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

       類載入器雖然只用於實現類的載入動作,但是它在Java程式中起的作用卻不僅僅是進行類載入。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立它在Java虛擬機器中的唯一性。簡單來說就是,一個類的class檔案被不同的兩個類載入器載入,那麼載入後的這兩個類就不“相等”,不是相同的類。

       使用雙親委派模型,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如java.lang.Object類,它存放在rt.jar中,無論哪一個類載入器要載入這個類,最終都會委派給模型最頂端的根類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類(始終被根類載入器載入)。相反,如果不使用雙親委派模型,由各個類載入器自己去載入的話,假如使用者編寫了一個稱為java.lang.Object的類,並放在ClassPath中,那系統中會出現多個不同的Object類,應用程式也會變的一片混亂。

轉載: https://www.cnblogs.com/fangfuhai/p/7230179.html