歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Java虛擬機類加載機制

Java虛擬機類加載機制

日期:2017/3/1 9:05:49   编辑:Linux編程

我是家寶

Java虛擬機類加載機制

定義

虛擬機把描述類的數據從Class文件加載到內存,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類加載的過程

類的加載過程分為5個步驟:加載、驗證、准備、解析、初始化

其中的驗證、准備、解析階段又統稱為連接,如下圖所示。

在這5個階段中,加載、驗證、准備、初始化這4個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定,為了支持java語言的運行時綁定,它在某些情況下可以在初始化階段之後再開始。

這裡之所以說按部就班地開始,而不是按部就班地“進行”或“完成”,是因為這些階段通常可以交叉混合進行,如在加載階段執行過程中,也會同時執行驗證階段。

類加載的時機

什麼時候要開始一個類的類加載?

什麼情況下需要開始類加載過程的第一步“加載”?Java虛擬機並沒有進行強制約束,這點可以由虛擬機自行實現。

但是對於初始化階段,虛擬機規范則進行了嚴格規定,有且只有在以下7種情況下,必須立即對類進行初始化(而加載、驗證、准備自然需要在此之前開始):

  1. 使用new關鍵字實例化對象的時候
  2. 設置或讀取一個類的靜態字段(被final修飾,已在編譯器把結果放入常量池的靜態字段除外)的時候
  3. 調用一個類的靜態方法的時候
  4. 使用java.lang.reflect包的方法對類進行反射調用的時候
  5. 初始化一個類的子類(會首先初始化父類)
  6. 當虛擬機啟動的時候,初始化包含main方法的主類
  7. 當使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

類加載過程詳解

加載

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

  1. 通過一個類的全限定名來獲取此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的入口(雖然是對象,但並沒有明確規定要存在java堆中,HotSpot的實現是存在了方法區)

驗證

驗證階段大致分4個階段:

  1. 文件格式驗證
  2. 元數據驗證
  3. 字節碼驗證
  4. 符號引用驗證

此階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流才會進入內存的方法區中進行存儲,所以後面3個階段全部是基於方法區的存儲結構進行的,不再直接操作字節流。(從這裡也可以看出,驗證階段是和加載階段一起進行的,只有當驗證階段完成後,加載階段的第二個步驟將字節流轉儲為方法區的運行時數據結構才能完成)

准備

准備階段是正式為類變量分配內存並設置類變量初始值。

需要注意的是,類似於:

“public static int value=123” 這種定義,在准備階段過後的初始值是0而不是123,把value賦值為123需要等到初始化階段再執行。

但是如果是:

“public static final int value=123” 這種定義,編譯時javac會給value生成ConstantValue屬性,這種情況下在准備階段虛擬機就會根據ConstantValue將value賦值為123。

解析

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

主要有4種:

  1. 類或接口的解析
  2. 字段解析
  3. 類方法解析
  4. 接口方法解析

初始化

初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中的靜態變量賦值動作和靜態語句塊(static{})中的語句合並生成的,編譯器的收集順序由語句在源文件中出現的順序決定。

需要注意的有兩點:

  1. 在執行類的<clinit>()方法之前,如果類有父類,則先執行父類的<clinit>()方法
  2. 與類不同,在執行接口的<clinit>()方法之前,如果接口有父接口,不需要先執行父類的<clinit>()方法。

類加載器

在類加載的第一個階段--“加載”階段,第一個動作是:“通過一個類的全限定名來獲取此類的二進制字節流”,我們把實現這個動作的代碼模塊稱為“類加載器”。

類加載器最初是為了滿足Java Applet的需求而開發的,雖然目前Java Applet基本已經“死掉”,但是類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩,成為Java體系中的一塊重要基石。

必須了解的一個概念是:

對於一個類,這個類本身和加載它的類加載器一同確立其在Java虛擬機的唯一性。

也就是說,不同類加載器加載同一個Class文件所產生的兩個類是不相等的,體現在Class對象的equals()方法、instanceof關鍵字的判定等。

類加載器分類

  1. 啟動類加載器(Bootstrap ClassLoader):負責將JAVA_HOME/lib 目錄下的,或者被-Xbootclasspath參數所指定的路徑中的,被虛擬機識別的類庫(僅按文件名識別,如rt.jar,名字不符合的類庫不會加載)加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用。
  2. 擴展類加載器(Extension ClassLoader):加載JAVA_HOME/lib/ext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  3. 應用程序類加載器(Application ClassLoader):也成為系統加載器。負責加載用戶類路徑(classpath)下所指定的類庫,開發者可以直接使用這個類加載器。如果應用程序中沒有自定義自己的類加載器,一般這個就是程序中默認的類加載器。

雙親委派模型

如圖:

圖中的類加載器之間的層次關系,稱為類加載器的雙親委派模型。

雙親委派模型要求除了頂層的啟動類加載器之外,其他的加載器都要有自己的父加載器。這裡類加載器之間的父子關系不以繼承而是以組合方式來實現。

雙親委派模型的工作過程是:每一個類加載器收到類加載請求,都會首先將請求委派到其父加載器去完成,只有當父加載器無法完成加載,子加載器才會嘗試自己去加載。

雙親委派模型的好處是Java類隨著它的加載器一起具備了優先級的層級關系。如java.lang.Object,它存在於rt.jar,無論哪個類加載器要加載這個類,最終都要委派給頂層的啟動加載器,因此Object在程序的各類加載器環境中都是同一個類。即使用戶自己定義一個java.lang.Object類,也無法被加載。

Copyright © Linux教程網 All Rights Reserved