JVM之類加載機制

       在上一篇文章中我們瞭解了Class文件存儲格式的具體細節,在Class文件中描述的各種信息,最終都要加載到虛擬機中之後才能運行和使用。而虛擬機如何加載這些Class文件?Class文件中的信息進入到虛擬機後會發生什麼變化?這些都是本文所需要解決的問題!

      虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化最終形成了可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類加載的時機

      類從北加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析三個部分統一稱爲Linking()鏈接。其中加載、驗證、準備、初始化和卸載這五個階段的順序是確定過的,類加載的過程必須按照這種順序執行,但是解析則不一定:解析有時候會在初始化階段之後再開始。

     那麼什麼時候需要開始類加載、驗證····初始化完全呢?有以下五種情況類就必須要初始化:

   (1)遇到new、getstatic、putstatic和invokestatic這4條指令時,翻譯過來就是:使用new創建對象時、讀取或者調用設置使用一個static字段時、以及調用static方法時。

   (2)使用反射時

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

   (4)當虛擬機啓動時,用戶需要指定一個要執行的主類(有main()方法的類),虛擬機會先初始化這個類。

   (5)當使用JDK1.7的動態語言支持時,如果一個MethodHandler實例後的解析結果REF_getstatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先初始化。

    JVM規範中規定,只有以上5種場景纔會觸發類的初始化,這五種行爲被稱爲主動行爲,初次之外,所有引用類的方式都不會觸發初始化,被稱爲被動引用,有兩個典型的被動引用例子:

   (1)在子類中引用父類的靜態字段,不會導致子類初始化。

   (2)常量在編譯階段會存儲調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。簡單點就是當調用static final定義的字段時,不會導致類的初始化。

類加載的過程

      接下來就詳細講解一下Java虛擬機中類加載的全過程,也就是加載、驗證、準備、解析和初始化這五個過程以及其具體的動作。

加載

      “加載”是“類加載”過程的一個階段,而這不能混淆,在加載過程中,虛擬機要完成以下三件事情:
    (1)通過一個類的全限定名來獲取定義此類的二進制字節流。
    (2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
    (3)在內存中生成一個代表這個類的java.lang.Class對象,作爲方法去這個類的各種數據的訪問入口。
     相對於類加載過程的其它階段,一個非數組類的加載階段(準確的說,是加載階段中獲取類的二進制字節流的)是開發人員可控制性最強的,因爲加載階段金額可以使用系統提供的引導類加載器來完成,也可以由用戶自定義的類加載器去完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式(重寫一個類加載器的loadClass()方法即可)。
     對於數組類而言,情況就有所不同,數組淚本身不通過類加載器創建,它是由虛擬機直接創建的,但數組類與類加載器仍然有很密切的關係,因爲數組類的元素類型最終是要靠類加載器去創建,一個數組類創建過程需要遵循以下規則:
    (1)如果數組的組建類型(指的是數組去掉一個維度的類型)是引用類型,那就遞歸採用本節中定義的加載過程去加載這個組件類型,數組C將在加載該組建的類加載器的類名稱空間上被標識。
    (2)如果數組的組建類型不是引用類型(例如int[] arr),Java虛擬機將會把數組C標記爲與引導加載器一起確定唯一性。
    (3)數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認爲public。
     類加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需要的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。然後在內存中實例化一個Class類的對象,Class對象比較特殊,它雖然是對象,但是存放在方法區裏,這個對象將作爲程序訪問方法區中的這些類型數據的外部接口。

驗證

      驗證時鏈接階段的第一部,這一階段的目地市確保Class文件的字節流中包含的信息符號當前虛擬機的要求,並且不會危害虛擬機自身安全。
      驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機是否能陳壽惡意代碼的攻擊,從執行性能的角度來說,驗證階段的工作量在虛擬機的類加載子系統中又佔類相當大的一部分,從整體來說,驗證階段主要包括以下四個階段。

文件格式的驗證

    (1)是否以魔術0xCAFEBABY開頭
    (2)主次版本是否在當前虛擬機處理範圍之內
    (3)常量池的常量是否有不被支持的常量類型
    (4)指向常量的各種索引值是否有指向不存在的常量或不符合類型的常量
    (5)Class文件中各個部分及文件本身是否被刪除的或附加的其他信息
       實際上,第一階段的驗證點遠遠不止這一點,這個惡階段的驗證時基於二進制字節流進行的,只有通過了這個階段的驗證之後,字節流纔會進入內存的方法區中進行存儲,所以後面三個驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流。

元數據驗證

       第二階段是對字節碼描述信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,這個階段可能包括以下驗證:
   (1)這個類是否有父類
   (2)這個類的父類是否繼承類不被允許繼承的類(被final修飾的類)
   (3)如果這個類不是抽象類,是否實現類其父類或接口之中要求實現的所有方法
   (4)類中的字段、方法是否與父類產生矛盾
       這個階段的主要目的是對類的元數據進行語義檢查,保證不存在不符合Java語言規範的元數據信息。

字節碼驗證

       第三階段是整個驗證中最複雜的一個階段,主要目地市通過數據流和控制流程分析,確定程序語義是合法的、符合邏輯的。在第二個階段對元數據信息中的數據類型昨晚校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。
    (1)保證任意時刻操作數棧的數據類型與質量代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作數棧放類一個int類型數據,使用時卻按long類型來加入本地變量表
    (2)保證跳轉指令不會跳轉到方法體以外的字節碼指令上
    (3)保證方法體重的類型轉換時有效的,例如可以把一個子類對象賦值給父類數據類型,這是安全的,但是把父類對象賦值給子類數據類型,這是危險並且不合法的/。
       如果一個類方法體字節碼沒有通過字節碼驗證,那肯定是有問題的:但如果一個方法體通過類字節碼驗證不一定就是安全的/。

符號引用驗證

       最後一個驗證發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在鏈接的第三個階段----解析階段中發生。符號引用驗證可以看作是對類自身以外的信息進行匹配性校驗。
    (1)符號引用中通過字符串描述的全限定名是否能找到對應的類
    (2)在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
    (3)符號引用中的類、字段、方法的訪問性是否可以被當前類訪問
     符號引用驗證的目地市確保解析動作能正常執行,如果無法通過符號引用驗證。那麼將會拋出如下異常:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
     對於虛擬機的類加載機制來說,驗證階段是一個非常重要的、但不是一定必要的階段。

準備

     準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中static類型的變量只是初始化爲“零”值,並不是初始化爲程序員指定的值。而實例變量在此時不會被初始化,它會被隨着對象一起分配在Java堆中。比如:public static int value =123,此時value被初始化爲0,而不是123。

解析

      解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,符號引用在前面講Class文件格式的時候講過:
     (1)符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用雨虛擬機實現的內存佈局無關,引用的目標不一定已經加載到內存,各種虛擬機實現的內存佈局可以各不相同,但是他們能接受的符號引用必須都是一致的。
     (2)直接引用:直接引用可以是直接指向目標的指針,相對便宜量或者是一個能簡介定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關,所以同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。
      解析都換工作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行

初始化

      類初始化階段是類加載過程的最後一步,前面的類加載過程中,除類在加載階段用戶應用程序可以通過自定義類加載器參與外,其餘動作完全由虛擬機主導完成。在準備階段變量已經賦值過一次系統要求的值,而在初始化階段,則根據程序員指定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始階段是執行類構造器方法的過程。

類加載器

      虛擬機設計團隊把類加載階段中的”通過一個類的全限定名來獲取描述此類的二進制字節流“這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊稱爲“類加載器”。

 類與類加載器

      類加載器雖然只用於實現類的加載動作,但是它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在JVM中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。即:比較兩個類是否相等,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則即使這兩個類來源於同一個文件,被同一個虛擬機加載,只要加載他們的類加載器不同,那麼這兩個類就一定不相同。
雙親委派模型
      從Java虛擬機的角度來說,只存在兩種不同的類加載器:一種是啓動類加載器,這個類加載器使用C++語言實現,是虛擬機自身的一部分,另一種是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自ClassLoader類。
     雙親委派模型是這樣的:如果一個類加載器收到了類加載的請求,它首先不會對自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子類加載器纔會嘗試自己去加載。更多的關於類加載器參考:http://www.cnblogs.com/lanxuezaipiao/p/4138511.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章