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類,應用程序也會變的一片混亂。

       以上內容總結了Java類加載機制的整個過程以及雙親委派模型的原理,歡迎交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM