天天看點

七、JVM(HotSpot)虛拟機類加載機制

注:本博文主要是基于JDK1.7會适當加入1.8内容。

虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗,轉換解析,初始化,最終形成可以被虛拟機直接使用的Java類型,這就是虛拟機的類加載機制。

1、類加載時機

類加載到記憶體開始直至解除安裝出記憶體,它的生命周期包括:加載、驗證、準備、解析、初始化、使用和解除安裝,其中驗證、準備、解析稱之為連接配接。

加載、驗證、準備、初始化、解除安裝這五個階段的順序是确定的,但是解析階段則不一定,某些時候可以再初始化之後再進行,符合Java語言運作時綁定的特征。

五種情況,進入初始化階段:

  • 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令,如果類沒有進行初始化,則需要先觸發初始化。
  • 使用java.lang.reflect包中方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發初始化。
  • 當初始化遇到一個類的時候,如果父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛拟機啟動時,使用者需要指定一個需要執行的主類(包含main()方法的類),虛拟機會先初始化這個主類。這其實類似于getstatic指令,main()方法也是定義為static類型的。
  • 當使用JDK1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後解析結果REF_GetStatic、REF_PutStatic、REF_invokeStatic方法句柄,并且這個方法句柄多對應的類沒有進行初始化,則需要先觸發初始化。

2、類加載過程

(1)加載

加載是類加載的一個階段,概念不要混淆。在加載階段虛拟機需要做3件事情:

  • 通過類的全限定名來擷取定義此類的二進制位元組流
  • 将這個位元組流所代表的靜态存儲結構轉換為方法區的運作時資料結構
  • 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料通路接口(存放在方法區,非Heap中)

對于數組類型而言,情況有所不同,數組類本身并不是通過類加載器建立,它是由Java虛拟機直接建立的,但數組類扔與類加載器有着很大的關聯,因為數組類的元素類型最終還是要通過類加載進行建立,一個數組類建立過程遵循以下規則:

  • 如果數組的元件類型是引用類型,那就遞歸采用本節中定義的家在過程去加載這個元件類型,數組元件類型将在加載該元件類型的類加載器的類名稱空間上被辨別
  • 如果數組的元件類型不是引用類型(如int[],二維數組,多元數組),Java虛拟機将會把數組元件類型标記為與引導類加載器關聯
  • 數組類的可見性和它的元件類型可見性一緻,如果元件類型不是引用類型,那數組類的可見性預設為public

(2)驗證

Java語言本身是安全性的語言,但是如果Class檔案并不是從Java檔案通過javac編譯而來,則并不能保證它的安全性,是以驗證Class檔案二進制流至關重要(java.lang.VerifyError)。驗證階段有4個階段的驗證:檔案格式驗證、中繼資料驗證、位元組碼驗證、符号引用驗證。

  • 檔案格式驗證,類檔案格式章節對Class檔案類格式做出了詳細的要求,可參考
  • 中繼資料驗證,對位元組碼描述的資訊進行語義分析。包括:這個類是否有父類,除了java.lang.Object其他類都應該有父類;這個類是否繼承了不被允許繼承的類,被final修飾的類;如果類不是抽象類是否實作了父類或者接口中要求實作的所有方法;類中字段,方法是否與父類産生沖突,父類final字段,不符合重載等
  • 位元組碼驗證,通過資料流和控制流分析,确定程式語義是合法的,符合邏輯的。包括:保證任意時刻操作數棧的資料類型與指令代碼序列能配合工作,不會出現操作數棧放置int類型資料,使用時卻按照long類型加載本地變量表中;保證跳轉指令不會跳轉到方法體以外的位元組碼指令上等
  • 符号引用,發生在虛拟機将符号引用轉化為直接引用時候,動作發生在連接配接的第三個階段——解析階段。符号引用驗證可以看做是對類自身以外的資訊進行比對性校驗,通常有一下内容;符号引用通過字元串描述的全限定名能否找到相應的類;指定類中是否存在符合字段描述符以及簡單名稱描述的方法和字段;符号引用中的類、字段、方法的通路性(private、protected、public)是否可以被目前類通路等

引用階段對虛拟機的類加載機制來說非常重要但非必須,如果運作的代碼已經被反複驗證通過,那麼實施階段可以考慮使用-Xverify:none參數關閉大部分的類驗證措施,縮短虛拟機類加載機制的時間。

(3)準備

正式為類變量配置設定記憶體并設定類變量初始值的階段,這些變量的記憶體都将在方法區中進行比對。注意:變量是指類變量非執行個體變量,通常情況下是資料類型的零值,即

public static int value = ;
           

在準備階段value的值是0,隻有經過初始化也就是類構造方法< clinit >方法後才會變成123。

(4)解析

虛拟機将常量池内的符号引用替換為直接引用的過程。解析動作主要針對類或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、類方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_Interfaceref_info)、方法類型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)、調用限定符(CONSTANT_InvokeDynamic_info)。

符号引用:以一組符号來描述鎖引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義的定位到目标即可。

直接引用:可以是直接指向目标的指針,先對偏移量或一個能間接定位到目标的句柄。

類或接口解析

假設目前代碼所處類,如果把一個為解析過的符号引用解析為一個類或接口的直接引用,步驟為:

第一步:如果類或接口不是一個數組類型,虛拟機會把符号引用的全限定名傳遞給代碼所在類的類加載器去加載這個類的符号引用。加載過程中,由于中繼資料驗證、位元組碼驗證的需要,可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的接口。一旦加載過程中出現了任何異常,解析過程宣告失敗。

第二步:如果類或接口是一個數組類型,并且數組的元素類型為對象,也就是符号引用的描述符會是類似“[Ljava/lang/Integer”的形式,将會按照第一步的規則加載數組元素類型。如果符号引用的描述符如前面鎖假設的形式,需要加載的元素類型為“java.lang.Integer”,接着虛拟機生成一個代表數組次元和元素的數組對象。

第三步:如果上面步驟沒有出現異常,那麼類或接口在虛拟機實際上已成為一個有效的類或者接口,但在解析完成之前還要進行符号引用驗證,确認代碼所在類是否具備對類或接口的通路權限。如果發現不具備通路權限,将抛出java.lang.IllegalAccessError異常。

字段解析

解析一個未被解析過的字段符号引用,首先對字段表内class_index項索引的CONSTANT_Class_info符号引用進行解析,如果解析成功,将對這個字段所屬的類或接口進行後續字段的搜尋。

第一步:如果類或接口本身包含了簡單名稱和字段描述符都與目标相比對的字段,傳回這個字段的直接引用,查找結束。

第二步:否則,如果類或接口中實作了接口,将按照繼承關系從下往上遞歸搜尋各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目标比對的字段,則傳回這個字段的直接引用,查找結束。

第三步:否則,如果類或接口不是java.lang.Object的話,按照繼承關系從下往上遞歸搜尋其父類,如果在父類中包含了簡單名稱和字段描述都與目标比對的字段,則傳回這個字段的直接引用,查找結束。

第四步:否則,查找失敗,抛出java.lang.NoSuchFieldError異常。

如果查找過程成功傳回了引用,還将進行權限驗證,如果發現不具備這個字段的通路權限,将抛出java.lang.IllegalAccessError異常。

類方法解析

解析一個未被解析過的類方法符号引用,首先對類方法表内class_index項索引的CONSTANT_Class_info符号引用進行解析,如果解析成功,将對這個類方法所屬的類方法的搜尋。

第一步:類方法和接口方法符号引用的常量類型定義是分開的,如果類方法表中發現class_index中索引是接口,直接抛出java.lang.IncompatibleClassChangeError異常。

第二步:通過第一步,在類中查找是否有簡單名稱和描述符都與目标比對的方法,如果有則傳回這個方法的直接引用,查找結束。

第三步:否則,在類的父類中遞歸查找是否有簡單名稱和描述符都與目标比對的方法,如果有則傳回這個方法的直接引用,查找結束。

第四步:否則,在類實作的接口清單即它們的父接口中遞歸查找是否有簡單名稱和描述符都比對的方法,如果有則傳回這個方法的直接引用,查找結束。

第五步:否則,查找失敗,抛出java.lang.NoSuchMethodError異常。

如果查找過程成功傳回了引用,還将進行權限驗證,如果發現不具備這個字段的通路權限,将抛出java.lang.IllegalAccessError異常。

接口方法解析

解析一個未被解析過的接口方法符号引用,首先對接口方法表内class_index項索引的CONSTANT_Class_info符号引用進行解析,如果解析成功,将對這個接口方法所屬的接口方法的搜尋。

第一步:與類方法解析不同,如果接口方法表中發現class_index項索引的CONSTANT_Class_info的方法所屬是類,将抛出java.lang.IncompatibleClassChangeError異常。

第二步:否則,在接口中查找是否有簡單名稱和描述符相比對的接口方法,如果有則傳回這個方法的直接引用,查找結束。

第三步:否則,在接口的父接口中遞歸查找,知道java.lang.Object類為止,是否有簡單名稱和描述符都與目标相比對的接口方法,如果有則傳回這個方法的直接引用,查找結束。

第四步:否則,查找失敗,抛出java.lang.NoSuchMethodError異常。

由于接口中的所有方法預設為public abstract,不會存在通路權限問題,是以接口方法的符号解析不會出現java.lang.IllegalAccessError異常。

(5)初始化

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

  • < clinit >()方法是由編譯器自動收集類中所有類變量的指派動作和靜态語句塊。編譯器收集順序是由語句在源檔案中出現的順序決定,靜态語句塊隻能通路到定義在靜态語句塊之前的變量,定義在之後的變量可以指派但不能通路。
  • < clinit >()方法與類的構造函數(執行個體構造函數< init >)不同,它不需要顯示地調用父類構造器,虛拟機會保證在子類< clinit >()方法執行之前,父類< clinit >()方法已經執行,是以在虛拟機中第一個執行的永遠是java.lang.Object的< clinit >()方法。
  • 父類< clinit >()方法先執行,意味父類中定義的靜态語句先于子類中靜态語句執行。
  • < clinit >()方法非必須,如果一個類或接口中沒有靜态語句塊也沒有對類變量的指派操作,編譯器不為這個類或接口生成< clinit >()方法。
  • 接口中不能使用靜态代碼塊,但仍然有變量初始化操作,是以和類一樣,接口也會生成< clinit >()方法。但是,不同的是,執行接口的< clinit >()方法不需要先執行父類< clinit >()方法,隻有父接口中的變量使用到時,才會執行< clinit >()方法。
  • 虛拟機保證一個類的< clinit >()方法在多線程環境中被正确的加鎖、同步,如果多個線程同時去初始化一個類,那麼隻有一個線程去執行這個類的< clinit >()方法,其他線程都需要阻塞等待,直到活動線程執行< clinit >()方法結束。

3、類加載器

通過一個類的全限定名來擷取描述此類的二進制位元組流的動作放到虛拟機外實作,以便讓應用程式決定如何去擷取所需要的類。實作這個動作的代碼子產品稱之為“類加載器”。

(1)類與類加載器

判斷兩個類是否相等的前提是類所在的類加載器是一緻的,如果不一緻,則類一定不相等。

(2)雙親委派模型

從Java虛拟機角度講,隻存在兩種不同的類加載器,一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實作;另一種是所有其他類加載器,由Java語言實作,獨立于虛拟機外部,并且繼承自抽象類java.lang.ClassLoader。

  • 啟動類加載器(Bootstrap ClassLoader):< JAVA_HOME >\lib。負責存放虛拟機識别的類庫加載到虛拟機記憶體中,啟動類加載器無法被Java程式直接引用,使用者在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,直接使用null代替即可。
  • 擴充類加載器(Extension ClassLoader):< JAVA_HOME >\lib\ext或者被java.ext.dirs系統變量指定路徑中的所有類庫。
  • 應用程式類加載器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader實作。它負責加載使用者類路徑上所指定的類庫。

雙親委派模型要求除了頂層啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。過程:如果一個類加載器收到類加載請求,首先不會自己嘗試加載這個類,而是把這個請求委派給父類加載器完成,每一層的類加載器都是如此,直到傳到頂層啟動類加載器中,隻有當父加載器回報無法完成這個加載請求,才會下放到子類加載器中執行。

(3)破壞雙親委派模型

  • 雙親委派機制在JDK1.2中引入,而類加載器和抽象類java.lang.ClassLoader則在1.0時代已經存在,需要滿足向前相容。java.lang.ClassLoader添加了一個新的protected findClass()方法,在此之前是使用者繼承java.lang.ClassLoader唯一目的是重寫loadClass()方法,因為虛拟機在進行類加載時調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯是調用自己的loaderClass()。
  • 模型自身缺陷所緻。出現基礎類調用回使用者代碼,怎麼辦?設計線程上下文類加載器(Thread Context ClassLoader),可以通過java.lang.Thread類的setContextClassLoader()設定。
  • 使用者追求程式動态性導緻,代碼熱替換、子產品熱部署等。OSGi環境中,類加載不再是雙親委派模型的樹狀結構,而是進一步發展成為更複雜的網狀結構。第一步,将以java.*開頭的類委托給父類加載器加載;第二步,否則将委派清單名單内的類委派給父類加載器加載;第三步,否則将import清單中的類委派給export這個類的Bundle的類加載器加載;第四步,否則查找目前Bundle的ClassPath,使用自己的類加載器加載;第五步,否則查找類是否在自己的FragmentBundle中,如果在則委派給FragmentBundle的類加載器加載;第六步,否則查找DynamicImport清單中的Bundle,委派給對應Bundle的類加載器加載;最後,類查找失敗。

繼續閱讀