JVM 類載入機制及雙親委派模型

sunkang發表於2019-03-04

概述

 java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別,這就是虛擬機器的類載入機制。
 
  上面這段是《深入理解java虛擬機器》中對類載入的描述,其實簡單點說就是程式從最開始的.java檔案到.Class檔案,Class檔案中包java虛擬機器指令集和符號表以及其他輔助資訊,而這些資訊最終載入到虛擬機器才能被使用。接下來我們就一起討論這些Class檔案如何被載入以及被載入後變成了什麼。
複製程式碼

類的生命週期

JVM 類載入機制及雙親委派模型
如上圖所示,描述了類的生命週期。其中載入、驗證、準備、初始化、解除安裝這五個動作是存在先後順序的,而解析階段有可能在初始化之後完成的。這些動作中通常都是互相交叉混合進行的。下面我們主要探討載入、驗證、準備、解析、初始化這五個步驟。

載入

  • 何時載入

    1.預載入:在虛擬機器啟動的時候載入,載入的是JAVA_HOME/lib/下的rt.class下的.class檔案,是java程式執行時經常要用到的一些類,比如java.lang.⁎以及 java.util.⁎等
    2.執行時載入:虛擬機器在用到一個.class檔案時,首先會去記憶體中查詢這個.class檔案有沒有被載入,沒有被載入會根據這個類的全限定名去載入。
    複製程式碼
  • 載入階段虛擬機器做了什麼

    1. 通過一個類的全限定名獲取定義此類的二進位制位元組流。
    2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行資料結構。
    3. 在記憶體中生成一個唯一代表此類的java.lang.Class物件,作為方法區中這個類的各種資料的訪問入口。(一般這個class物件會儲存在堆中,不過HotSpot虛擬機器比較特殊,這個Class物件是放在方法區中的。)
    複製程式碼
虛擬機器對上述三點的要求並不算具體,例如第一條,根本沒指明二進位制位元組流從哪裡來,怎麼來,包括以下幾點:
     從zip包中獲取,這就是以後jar、ear、war格式的基礎
     從網路中獲取,典型應用就是Applet
     執行時計算生成,典型應用就是動態代理技術
     由其他檔案生成,典型應用就是JSP,即由JSP生成對應的.class檔案
     從資料庫中讀取,這種場景比較少見...
複製程式碼

驗證

 顧名思義,是對Class檔案位元組流的驗證,而驗證的目的則是為了確保當前的Class檔案符合java虛擬機器的要求,並且不會危害虛擬機器自身的安全。通常主要包括以下幾點驗證內容:
   1. 檔案格式驗證
      其實就是驗證位元組流是否符合Class檔案規範,符合規範通過驗證才能保證輸入的位元組流能正確的被解析並儲存到方法區。
   2. 後設資料驗證
      對類的後設資料資訊進行語義校驗。
   3. 位元組碼驗證
    最為複雜的校驗階段,校驗程式語義是否符合規範,符合邏輯,對類的方法體進行校驗。  
   4. 符號引用驗證
     發生在將符號引用轉換為直接引用的時候,可以看做是對類自身以外(常量池中各種符號的應用)資訊的匹配校驗,如:符號引用中通過字串描述的全限定名是否能找到對應的類;符號引用中的類、欄位、方法的訪問性(private、protected、public、default)是否可被 當前類訪問...
複製程式碼

準備

 正式為類變數分配記憶體並賦初值。
 需要注意兩點
   1. 只為類變數,即被static修飾的變數分配記憶體,例項變數在例項初始化的時候會隨物件一起分配在堆中。
   2. 這個階段賦初始值的變數指的是那些不被final修飾的static變數,比如”public static int value = 123;”,value在準備階段過後是0而不是123,給value賦值為123的動作將在初始化階段才進行;比如”public static final int value = 123;”就不一樣了,在準備階段,虛擬機器就會給value賦值為123。
   基本資料的零值如下表:
複製程式碼

JVM 類載入機制及雙親委派模型

解析

 解析是虛擬機器將常量池中的符號引用轉換為直接引用的過程。
   1. 符號引用
   這個其實是屬於編譯原理方面的概念,符號引用包括了下面三類常量:
     類和介面的全限定名
     欄位的名稱和描述符
     方法的名稱和描述符
複製程式碼

JVM 類載入機制及雙親委派模型
看到Constant Pool也就是常量池中有22項內容,其中帶”Utf8″的就是符號引用。比如#2,它的值是”com/xrq/test6/TestMain”,表示的是這個類的全限定名;又比如#5為i,#6為I,它們是一對的,表示變數時Integer(int)型別的,名字叫做i;#6為D、#7為d也是一樣,表示一個Double(double)型別的變數,名字為d;#18、#19表示的都是方法的名字。

那其實總而言之,符號引用和我們上面講的是一樣的,是對於類、變數、方法的描述。符號引用和虛擬機器的記憶體佈局是沒有關係的,引用的目標未必已經載入到記憶體中了。

   2. 直接引用
     直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器示例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經存在在記憶體中了。
複製程式碼

初始化

 開始真正的執行類中定義的java程式碼,初始化過程就是執行類構造器<clinit>()的過程,還記得之前的準備階段是給類變數分配記憶體賦初值,這裡就是將類變數賦予使用者指定的值
 。
 虛擬機器規範定義了“有且僅有”5中會觸發初始化的場景:
    1. 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果累沒有進行初始化,則要先觸發初始化;
    2. 使用java.lang.reflect包中的方法對類進行反射呼叫的時候;
    3. 初始化類時,若發現其父類還沒有初始化,則先觸發父類的初始化;
    4. 虛擬機器啟動的時候,虛擬機器會先初始化使用者指定的包含main()方法的那個類
    5. 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
複製程式碼

雙親委派模型

 介紹雙親委派模型之前先說下類載入器。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立在jvm中的唯一性,每一個類載入器,都有一個獨立的類名稱空間。類載入器就是根據指定全限定名稱將class檔案載入到JVM記憶體,轉為Class物件。
 從jvm角度來看只存在兩種類載入器
複製程式碼
  • 啟動類載入器(Bootstrap ClassLoader),是虛擬機器自身的一部分,用來載入JAVA_HOME/lib/目錄中的,或者被-Xbootclasspath引數所指定的路徑中並且被虛擬機器識別的類庫。

  • 其他類載入器:由Java語言實現,繼承自抽象類ClassLoader:

    1. 擴充套件類載入器(Extension ClassLoader):負責載入<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變數指定的路徑中的所有類庫。
    2. 應用程式類載入器(Application ClassLoader)。負責載入使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類載入器。一般情況,如果我們沒有自定義類載入器預設就是用這個載入器。

JVM 類載入機制及雙親委派模型

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去載入這個類,而是把這個請求委派給父類載入器去完成,每一層的類載入器都是如此,這樣所有的載入請求都會被傳送到頂層的啟動類載入器中,只有當父載入無法完成載入請求(它的搜尋範圍中沒找到所需的類)時,子載入器才會嘗試去載入類。下面舉一個大家都知道的例子說明為什麼要使用雙親委派模型。

黑客自定義一個java.lang.String類,該String類具有系統的String類一樣的功能,只是在某個函式稍作修改。比如equals函式,這個函式經常使用,如果在這這個函式中,黑客加入一些“病毒程式碼”。並且通過自定義類載入器加入到JVM中。此時,如果沒有雙親委派模型,那麼JVM就可能誤以為黑客自定義的java.lang.String類是系統的String類,導致“病毒程式碼”被執行。

而有了雙親委派模型,黑客自定義的java.lang.String類永遠都不會被載入進記憶體。因為首先是最頂端的類載入器載入系統的java.lang.String類,最終自定義的類載入器無法載入java.lang.String類。

或許你會想,我在自定義的類載入器裡面強制載入自定義的java.lang.String類,不去通過呼叫父載入器不就好了嗎?確實,這樣是可行。但是,在JVM中,判斷一個物件是否是某個型別時,如果該物件的實際型別與待比較的型別的類載入器不同,那麼會返回false。

舉個簡單例子:

ClassLoader1、ClassLoader2都載入java.lang.String類,對應Class1、Class2物件。那麼Class1物件不屬於ClassLoad2物件載入的java.lang.String型別。

如何實現雙親委派模型

    雙親委派模型的原理很簡單,實現也簡單。每次通過先委託父類載入器載入,當父類載入器無法載入時,再自己載入。其實ClassLoader類預設的loadClass方法已經幫我們寫好了,我們無需去寫。
複製程式碼

破壞雙親委派模型

 雙親委派模型並不是一個強制性約束,而是java設計者推薦給開發者的類載入器的實現方式,在一定條件下,為了完成某些操作,可以“破壞”模型。
     1.重新loadClass方法
     2.利用執行緒上下文載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的 setContextClassLoaser()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承 一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式 類載入器。
     3.為了實現熱插拔,熱部署,模組化,意思是新增一個功能或減去一個功能不用重啟,只需要把這模組連同類載入器一起換掉就實現了程式碼的熱替換。
複製程式碼

本文參考《深入理解java虛擬機器》

[深度分析Java的ClassLoader機制(原始碼級別)](http://www.hollischuang.com/archives/199)

[Java類的載入、連結和初始化](http://www.hollischuang.com/archives/201)

相關文章