虛擬機類加載機制

閱讀《深入理解Java虛擬機-JVM高級特性與最佳實踐》.周志明 筆記

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

   -----引自 《深入理解Java虛擬機-JVM高級特性與最佳實踐》第7章、

Java語言裏面,類型的加載、連接和初始化過程都是在程序運行期間完成的。Java依賴運行期動態加載和動態鏈接這個特點實現動態擴展。如:編寫一個面向接口的程序,可以等到運行時再指定其實際的實現類。用戶可以通過Java預定義的和自定義類加載器,讓一個本地的應用程序可以在運行時從網絡或者其他地方加載一個二進制流作爲程序代碼的一部分,這種組裝應用程序的方式目前廣泛應用於Java程序之中。

類加載的是時機

類從被加載到虛擬機內存中開始,到卸載出內存,整個生命週期包括:加載、連接(驗證、準備、解析)、初始化、使用、卸載。

虛擬機規範並沒有進行強制約束什麼時候加載(第一個步驟)。但是對初始化階段室友嚴格規定的,有5種情況必須立即對類“初始化”(而加載、驗證、準備自然需要在此之前開始)。爲什麼沒有提到解析呢?因爲解析這個階段在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定(也稱之爲動態綁定或者晚期綁定)。那就先來看看5種必須立即對類進行“初始化”的情況:

1)遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時,如果沒有進行過初始化,則需要先觸發初始化。生成這4條指令最常見的Java代碼場景是:使用new(但是new 數組除外,因爲new數組的字節碼指令關鍵字是newarray)關鍵字實例化對象的時候、讀取或者設置一個類的靜態字段(僅自己,不包括繼承的)(被final修飾、已在編譯期吧結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候;

2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發初始化;

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

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

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

方便記憶期間:當啓動一個虛擬機進程(一個Java程序啓動的時候),首先要使用到主類(包含有main()方法的類),虛擬機首先必然要初始化該類。該類當中必然會使用到其他類可能會是三種方式使用到其他類:(1)new()一個對象時候,發現new的類還沒有初始化,那麼要初始化或者使用到一個類的static變量或方法(非final,因爲final static在編譯期已經進入到常量池了)時候發現該類沒有初始化;(2)初始化一個類的時候,發現父類還沒有初始化,那麼需要先初始化父類;(3)如果一個java.lang.invoke.Methodhandle實例最後解析結果REF_getStati、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

類加載的過程

Java虛擬機中類加載的全過程,也就是:加載、驗證、準備、解析、和初始化5個階段。

加載

加載階段,虛擬機需要完成以下3件事情:

1)通過一個類的全限定名來獲取定義此類的二進制字節流(.class文件或者其他);

2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;

3)在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

這裏說的類就是普通類。但是在JVM中對數組類的加載,情況就有所不同了,數組類本身不是通過類加載器創建,它是由Java虛擬機直接創建的。但是數組類與類加載器仍然有很密切的關係,因爲數組類的他不屬於類型(Element Type,指的是數組去掉所有維度的類型)最終是要靠類加載器去創建,一個數組類(記爲C)創建過程就遵循以下規則:

  • 如果數組的組件類型(Component Type,指數組去掉一個維度的類型)是引用類型,那就是遞歸採用上上文中加載過程去加載這個組件類型,數組C將在加載該組件類型的類加載器的類名稱空間被標識(一個類必須與類加載器一起確定唯一性);
  • 如果數組的組件類型不是引用類型(如int[]數組),Java虛擬機將會把數組C標記爲與引導類加載器關聯;
  • 數組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數組類的可見性將默認爲public;

加載階段完成之後,虛擬機外部的二進制字節流就按照虛擬機所需要的個事存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規範未規定此區域的具體數據結構。然後在內存中實例化一個java.lang.Class類的對象(並沒有明確規定是Java堆中,對於HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區裏面),這個對象將作爲程序訪問方法區中的這些類型數據的外部接口。

加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始了,但這些夾在加載階段之中進行的動作,任然屬於連接階段的內容,這連個階段的開始時間仍然保持着固定的先後順序。

驗證

驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身安全。

對於數組越界訪問等操作,編譯器會將其拒絕。但是Class文件產生的方式有多種,比如某強者直接用十六進制寫出來的Class文件。那麼在字節碼層面就可以訪問到數組界限之外的內存,所以爲了避免這樣的危險,驗證是虛擬機對自身保護的一項重要工作。

從執行的性能角度講:驗證階段的工作量在虛擬機的類加載子系統中又佔用了相當大一部分。主要有:

文件格式驗證:主要保證輸入的字節流能正確的解析並存儲於方法區內,格式上符合一個Java類型信息要求;

元數據驗證:對類的遠數據信息進行語義校驗,保證不存在不符合Java語言過飯的元數據信息,其中元數據(Metadata)包括:類、字段、方法定義以及其他信息。;

字節碼驗證:通過分析數據流和控制流分析,確定程序語義是合法的,符合邏輯的(針對類的方法,保證被校驗的方法在運行時不會做出危害虛擬機安全的實踐);

符號引用驗證:這一階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三個階段(解析)中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性驗證,通常需要校驗下列內容:

  • 符號引用中通過字符串描述的全限定名對否能找到對應類;
  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段;
  • 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問。

總之,符號引用驗證的目的是確保解析動作能夠正常執行。

需要重點說明的是,對於虛擬機的類加載機制來說,驗證階段是一個非常重要的階段、但不知一定必須(因爲對程序運行期間沒有什麼影響)的階段。如果所運行的全部代碼(包括自己編寫的以及第三方包中的代碼)都已經被反覆使用和驗證過,那麼在實施階段可以考慮用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間

準備

準備階段是正式爲類變量分配內存並設置類變量(被static修飾的變量)初始值(可以理解爲默認值,比如)的階段,這些變量所示的內存將在方法區中進行分配。

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。 那麼直接引用和符號引用的區別是什麼呢?

符號引用:符號引用是一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種虛擬機實現的內存佈局各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中

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

初始化

初始化階段是類加載的最後一步,前面幾個步驟,除了加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘的動作完全由虛擬機主導和控制。到了初始化階段,纔開始執行類中定義的Java程序代碼(或者說是字節碼)。從另外一個角度來講,初始化階段是執行類構造器<clinit>()方法的過程。

類加載器

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

類與類加載器

類加載器雖然只用於實現類的加載動作,但是它在Java程序中起到的作用遠遠不限於類加載階段。任何一個類,都需要由它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。如果要比較兩個類“是否相等”,如果不是由同一個類加載器來加載,那就必定不相等(即便是從同一個Class文件加載)。

雙親委派模型

從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都是由Java原因實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。站在開發人員角度來講,類加載器可以細分,絕大部分Java程序都會使用到以下3種系統提供的類加載器:

啓動類加載器:這個類加載器負責將存放在<JAVA_HOME>\lib目錄中,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用,用戶在編寫自定義的類加載器是,如果需要把加載請求委派給引導類加載器,那直接使用null替代即可;

擴展類加載器:這個類加載器 由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器;

應用程序類加載器:這個類加載器由sun.misc.Launcher$ApplicationClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被稱爲系統類加載器。它負責加載用戶類路徑(ClasssPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委派模型

 如上圖的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型。雙親委派模型要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏的加載器之間的父子關係一般不會以繼承的關係來實現,而都是使用組合關係來複用父類加載器的代碼。

雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類(findclass),而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有類加載器請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器纔會自己嘗試加載。

雙親委派模型對於保證Java程序的穩定運行有很重要的意義。但是雙親委派並不是一個強制的約束模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章