1. 程式人生 > >jvm之java類載入機制和類載入器(ClassLoader)的詳解

jvm之java類載入機制和類載入器(ClassLoader)的詳解

     當程式主動使用某個類時,如果該類還未被載入到記憶體中,則JVM會通過載入、連線、初始化3個步驟來對該類進行初始化。如果沒有意外,JVM將會連續完成3個步驟,所以有時也把這個3個步驟統稱為類載入或類初始化。

                                                    

一、類載入過程

1.載入    

    載入指的是將類的class檔案讀入到記憶體,併為之建立一個java.lang.Class物件,也就是說,當程式中使用任何類時,系統都會為之建立一個java.lang.Class物件。

    類的載入由類載入器完成,類載入器通常由JVM提供,這些類載入器也是前面所有程式執行的基礎,JVM提供的這些類載入器通常被稱為系統類載入器。除此之外,開發者可以通過繼承ClassLoader基類來建立自己的類載入器。

    通過使用不同的類載入器,可以從不同來源載入類的二進位制資料,通常有如下幾種來源。

  • 從本地檔案系統載入class檔案,這是前面絕大部分示例程式的類載入方式。
  • 從JAR包載入class檔案,這種方式也是很常見的,前面介紹JDBC程式設計時用到的資料庫驅動類就放在JAR檔案中,JVM可以從JAR檔案中直接載入該class檔案。
  • 通過網路載入class檔案。
  • 把一個Java原始檔動態編譯,並執行載入。

    類載入器通常無須等到“首次使用”該類時才載入該類,Java虛擬機器規範允許系統預先載入某些類。

2.連結

    當類被載入之後,系統為之生成一個對應的Class物件,接著將會進入連線階段,連線階段負責把類的二進位制資料合併到JRE中。類連線又可分為如下3個階段。

    1)驗證:驗證階段用於檢驗被載入的類是否有正確的內部結構,並和其他類協調一致。Java是相對C++語言是安全的語言,例如它有C++不具有的陣列越界的檢查。這本身就是對自身安全的一種保護。驗證階段是Java非常重要的一個階段,它會直接的保證應用是否會被惡意入侵的一道重要的防線,越是嚴謹的驗證機制越安全。驗證的目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,不會危害虛擬機器自身安全。其主要包括四種驗證,檔案格式驗證,元資料驗證,位元組碼驗證,符號引用驗證。

    四種驗證做進一步說明:

    檔案格式驗證:主要驗證位元組流是否符合Class檔案格式規範,並且能被當前的虛擬機器載入處理。例如:主,次版本號是否在當前虛擬機器處理的範圍之內。常量池中是否有不被支援的常量型別。指向常量的中的索引值是否存在不存在的常量或不符合型別的常量。

    元資料驗證:對位元組碼描述的資訊進行語義的分析,分析是否符合java的語言語法的規範。

    位元組碼驗證:最重要的驗證環節,分析資料流和控制,確定語義是合法的,符合邏輯的。主要的針對元資料驗證後對方法體的驗證。保證類方法在執行時不會有危害出現。

    符號引用驗證:主要是針對符號引用轉換為直接引用的時候,是會延伸到第三解析階段,主要去確定訪問型別等涉及到引用的情況,主要是要保證引用一定會被訪問到,不會出現類等無法訪問的問題。

   2)準備:類準備階段負責為類的靜態變數分配記憶體,並設定預設初始值。

   3)解析:將類的二進位制資料中的符號引用替換成直接引用。說明一下:符號引用:符號引用是以一組符號來描述所引用的目標,符號可以是任何的字面形式的字面量,只要不會出現衝突能夠定位到就行。佈局和記憶體無關。直接引用:是指向目標的指標,偏移量或者能夠直接定位的控制代碼。該引用是和記憶體中的佈局有關的,並且一定載入進來的。

3.初始化

    初始化是為類的靜態變數賦予正確的初始值,準備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執行過程是這樣的,首先位元組碼檔案被載入到記憶體後,先進行連結的驗證這一步驟,驗證通過後準備階段,給a分配記憶體,因為變數a是static的,所以此時a等於int型別的預設初始值0,即a=0,然後到解析(後面在說),到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。

二、類載入時機

  1. 建立類的例項,也就是new一個物件
  2. 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  3. 呼叫類的靜態方法
  4. 反射(Class.forName("com.lyj.load"))
  5. 初始化一個類的子類(會首先初始化子類的父類)
  6. JVM啟動時標明的啟動類,即檔名和類名相同的那個類    

     除此之外,下面幾種情形需要特別指出:

     對於一個final型別的靜態變數,如果該變數的值在編譯時就可以確定下來,那麼這個變數相當於“巨集變數”。Java編譯器會在編譯時直接把這個變量出現的地方替換成它的值,因此即使程式使用該靜態變數,也不會導致該類的初始化。反之,如果final型別的靜態Field的值不能在編譯時確定下來,則必須等到執行時才可以確定該變數的值,如果通過該類來訪問它的靜態變數,則會導致該類被初始化。

三、類載入器

    類載入器負責載入所有的類,其為所有被載入記憶體中的類生成一個java.lang.Class例項物件。一旦一個類被載入如JVM中,同一個類就不會被再次載入了。正如一個物件有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識。在Java中,一個類用其全限定類名(包括包名和類名)作為標識;但在JVM中,一個類用其全限定類名和其類載入器作為其唯一標識。例如,如果在pg的包中有一個名為Person的類,被類載入器ClassLoader的例項kl負責載入,則該Person類對應的Class物件在JVM中表示為(Person.pg.kl)。這意味著兩個類載入器載入的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所載入的類也是完全不同、互不相容的。

   JVM預定義有三種類載入器,當一個 JVM啟動的時候,Java開始使用如下三種類載入器:

 1)根類載入器(bootstrap class loader):它用來載入 Java 的核心類,是用原生程式碼來實現的,並不繼承自 java.lang.ClassLoader(負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類)。由於引導類載入器涉及到虛擬機器本地實現細節,開發者無法直接獲取到啟動類載入器的引用,所以不允許直接通過引用進行操作。

下面程式可以獲得根類載入器所載入的核心類庫,並會看到本機安裝的Java環境變數指定的jdk中提供的核心jar包路徑:

public class ClassLoaderTest {

	public static void main(String[] args) {
		
		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for(URL url : urls){
			System.out.println(url.toExternalForm());
		}
	}
}

執行結果:

  2)擴充套件類載入器(extensions class loader):它負責載入JRE的擴充套件目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。由Java語言實現,父類載入器為null。

  3)系統類載入器(system class loader):被稱為系統(也稱為應用)類載入器,它負責在JVM啟動時載入來自Java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH換將變數所指定的JAR包和類路徑。程式可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取系統類載入器。如果沒有特別指定,則使用者自定義的類載入器都以此類載入器作為父載入器。由Java語言實現,父類載入器為ExtClassLoader。

類載入器載入Class大致要經過如下8個步驟:

  1. 檢測此Class是否載入過,即在緩衝區中是否有此Class,如果有直接進入第8步,否則進入第2步。
  2. 如果沒有父類載入器,則要麼Parent是根類載入器,要麼本身就是根類載入器,則跳到第4步,如果父類載入器存在,則進入第3步。
  3. 請求使用父類載入器去載入目標類,如果載入成功則跳至第8步,否則接著執行第5步。
  4. 請求使用根類載入器去載入目標類,如果載入成功則跳至第8步,否則跳至第7步。
  5. 當前類載入器嘗試尋找Class檔案,如果找到則執行第6步,如果找不到則執行第7步。
  6. 從檔案中載入Class,成功後跳至第8步。
  7. 丟擲ClassNotFountException異常。
  8. 返回對應的java.lang.Class物件。

四、類載入機制:

1.JVM的類載入機制主要有如下3種。

  • 全盤負責:所謂全盤負責,就是當一個類載入器負責載入某個Class時,該Class所依賴和引用其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入。
  • 雙親委派:所謂的雙親委派,則是先讓父類載入器試圖載入該Class,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父載入器,依次遞迴,如果父載入器可以完成類載入任務,就成功返回;只有父載入器無法完成此載入任務時,才自己去載入。
  • 快取機制。快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入緩衝區中。這就是為很麼修改了Class後,必須重新啟動JVM,程式所做的修改才會生效的原因。

2.這裡說明一下雙親委派機制:

       雙親委派機制,其工作原理的是,如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行,如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器,如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己才想辦法去完成。

      雙親委派機制的優勢:採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。其次是考慮到安全因素,java核心api中定義型別不會被隨意替換,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類載入器,而啟動類載入器在核心Java API發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞的過來的java.lang.Integer,而直接返回已載入過的Integer.class,這樣便可以防止核心API庫被隨意篡改。