JVM類加載機制

JVM類加載機制

虛擬機把描述類的數據從Class文件加載到內存中,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

類從被加載到虛擬機內存中開始到卸載出內存爲止,整個生命週期包括:加載,驗證,準備,解析,初始化,使用和卸載7個階段。其中驗證,準備和解析3個部分成爲連接。
對類初始化的5種情況,有且僅有這5種

  • 遇到new,getstatic,putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發初始化。生成這4條指令最常見的場景是:使用new關鍵字實例化對象,讀取或設置一個類的靜態字段,以及調用一個類的靜態方法的時候
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發初始化
  • 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發父類初始化
  • 當虛擬機啓動時,用戶需要指定一個要執行的父類,虛擬機會啓動先初始化這個類
  • 當使用JDK1.7的動態語言支持時,如果一個java…lang.invoke.MethodHandle實例最後的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化

1.類加載的過程

1.1加載

加載是類加載過程的一個階段,在加載階段,虛擬機需要完成以下3件事

  1. 通過一個類的全限定名來獲取此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口

1.2驗證

驗證是連接的第一步,這個階段的目的是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,並且不會危害到虛擬機自身的安全

1.2.1文件格式校驗

第一階段要校驗字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理
例如:是否是以魔數oxCAFEBABE開頭
這個階段驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內,格式上符合描述一個Java類型信息的要求。這個階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲。

1.2.2 元數據驗證

第二階段是對字節碼進行語義進行分析,以確保其描述的信息符合Java語言的規範要求
例如:這個類是否有父類,這個類的父類是否繼承了不允許被繼承的類

1.2.3 字節碼驗證

字節碼驗證主要是通過數據流和控制流分析,確保程序語義是合法,符合邏輯的。這個階段將對類的方法體進行校驗分析,確保被校驗類的方法在運行時不會做出危害虛擬機安全的事件
例如:確保任意時刻操作數棧的數據類型與指令代碼序列都能配合工作;保證跳轉指令不會跳轉到方法體以外的字節碼指令上…

1.2.4 符號引用驗證

最後一個階段驗證發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三個階段—解析階段發生。符號引用可以看作是對類自身以外的信息進行匹配性校驗
例如:符號引用中通過字符串描述的全限定名是否能找到對應的類

1.3 準備

準備階段是爲正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區進行分配。

  1. 這個階段進行分配內存的僅是類變量,而不包括實例變量,實例變量將在對象實例化時隨對象一起分配在Java堆中。
  2. 這裏所說的初始化是數據結構的零值

1.4 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程
直接引用和符號引用的關聯

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時無歧義地定位到目標即可。符號引用與虛擬機是實現的內存佈局無關,引用的目標不一定已經加載到內存中
  • 直接引用:直接引用可以是直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄,直接引用和虛擬機實現的內存佈局有關,如果直接引用,那引用的目標必定已經在內存中存在

解析動作主要是針對類或接口,字段,類方法,接口方法,方法類型,方法句柄和調用點限定符7類符號引用進行的。

1.5 初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段用戶應用可以通過自定義類加載器參與之外,其餘動作都是虛擬機主導和控制的。到了初始化階段,才真正開始執行類中定義的Java程序代碼
初始化階段是程序員通過程序制定主觀計劃去初始化類變量和其他資源或者說是初始化階段是執行類構造器()方法的過程。

2 類加載器

虛擬機團隊把類加載階段中“通過一個類的全限定名來獲取描述此類的二進制字節流”的這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類,實現這個動作的代碼模塊稱爲類加載器

2.1 類與類加載器

對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每個類加載器都擁有一個獨立的類空間名稱。

2.2 雙親委派模型

雙親委派模型的工作過程:如果一個類加載器收到類加載請求,它首先不會自己去嘗試加載這個類,而是把請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此如果所有的類加載器請求最終都是應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試自己去加載。

  • 啓動類加載器:這個加載器負責將存在<JAVA_HOME>\lib目錄中,或被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機內存中。
  • 擴展類加載器:這個加載器由ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器
  • 應用程序類加載器:這個類加載器由AppClassLoader實現,這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值。它負責加載用戶類路徑上所指定的類庫,開發者可以直接使用這個類加載器

2.3 破壞雙親委派模型

雙親委派模型出現過三次較大規模“被破壞”情況

  • 第一次是雙親委派模型第一次被破壞是在雙親委派模型之前出現,雙親委派機制是在JDK1.2以後才被引入的,而類加載器和抽象類ClassLoader則在1.0時代出現的,面對已經存在的用戶自定義類加載器的實現代碼。Java設計者引入雙親委派模型時不得不進行一些妥協,先前兼容。因此在JDK1.2之後ClassLoader中添加了一個Protecte方法findClass(),在此之前,用戶去繼承ClassLoader的唯一目的是重寫loadClass()方法,因爲虛擬機在進行類加載的時候會調用加載器的私有構造方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass
    JDK1.2以後不在建議用戶直接去覆蓋loadClass()方法,而是應當把自己的類加載邏輯寫到findClass()中,loadClass()加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載是符合雙親委派模型的。

  • 第二次是由於模型自身缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類統一問題,但是會出下基礎類又要回調回用戶的代碼的問題,爲了解決這個問題,Java涉及團隊只好引入了一個不太優雅的設計:線程上下文類加載器。這個類加載器可以通過Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

  • 第三次是由於對程序動態性的追求而導致的,這裏所說的動態性是指:代碼熱替換,模塊熱部署等。OSGi實現熱模塊化部署的關鍵是它自定義的類加載器機制的實現。每一個模塊都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器也一起換掉實現代碼的熱替換。在OSGi環境下,類加載器不在使用雙親委派模型中的樹狀結構,而是進一步發展未更加複雜的網狀結構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章