筆記-JVM的類加載過程

Java代碼運行在JVM之上,JVM的運行情況對於Java程序至關重要。因此掌握JVM中的關鍵機制會對編寫穩定的,高性能的Java程序至關重要。

JVM規範中定義的標準結構如下圖所示:


JVM負責裝載class文件並執行,class文件通常由類加載器(ClassLoader)來完成加載。class的執行在Sun JDK中有解析執行和編譯爲機器碼執行兩種方式。其中編譯執行又分爲client和server兩種模式。

類加載機制可在運行時動態加載外部的類,遠程網絡下載過來的class文件等。除了動態加載,還可以通過JVM的類加載機制來達到類隔離的效果。

JVM將類加載分爲三個步驟:裝載,鏈接和初始化。裝載和連接過程完成後,即將二進制的字節碼轉換爲Class對象。初始化過程不是加載類時必須觸發的,但必須在初次使用該對象前完成。如下圖所示:


裝載

裝載時,JVM通過類的全名和類加載器來完成類的加載。同時也採用【類全名+ClassLoader實例ID】來標識一個被加載的類。

數組類型名稱爲:"[" +(基本類型或L+引用類型類名)。

鏈接

鏈接過程對二進制字節碼格式進行校驗,初始化裝載類中的靜態變量以及解析類中調用的接口,類。

二進制字節碼的格式校驗遵循Java Class File Format規範,如果格式不符合,則拋出VerifyError。校驗過程中如果碰到要引用的其他類和接口,也會進行加載。如果加載失敗,則會拋出NoClassDefFoundError。

完成校驗後,JVM初始化類中的靜態變量,並將其值賦爲默認值。

最後對類中的所有屬性,方法進行驗證,以確保其調用的屬性和方法存在,以及具備相應的權限。如果這個階段失敗,會拋出NoSuchMethodError,NoSuchFieldError等異常。

初始化

初始化過程即執行類中的靜態初始化代碼,構造期待嗎以及靜態屬性的初始化。以下四種情況下初始化過程會被觸發:

  • 調用了new。
  • 反射調用了類中的方法。
  • 子類調用了初始化。
  • JVM啓動過程中指定的初始化類。

JVM的類加載通過ClassLoader及其子類來完成,分爲Bootstrap ClassLoader,Extension ClassLoader,System ClassLoader以及User-Defined ClassLoader。這四種ClassLoader的關係如下圖所示:


Bootstrap ClassLoader

Sun JDK採用C++實現此類,此類並非ClassLoader的子類,在代碼中沒有辦法拿到這個對象。Sun JDK啓動時會初始化這個ClassLoader,並由ClassLoader完成$JAVA_HOME中jre/lib/rt.jar裏所有class文件的加載,jar中包含了Java規範定義的所有的接口和實現。

Extension ClassLoader

JVM用此ClassLoader來加載擴展功能的一些jar包,如Sun JDK中目錄下有dns工具jar包等,在Sun JDK中ClassLoader對應的類名爲ExtClassLoader。

System ClassLoader

JVM用此ClassLoader來加載啓動參數中指定的ClassPath中的jar包及目錄,在Sun JDK中ClassLoader對應的類名爲AppClassLoader。

User-Defined ClassLoader

它是由開發人員繼承ClassLoader抽象類自行實現的ClassLoader,基於自定義的ClassLoader可用於加載非Classpath中的jar及目錄。還可以在加載之前對class文件做一些動作,例如解密等。

JVM的ClassLoader採用的是樹形結構,如上圖所示。加載類時通常按照從上到下的原則進行。也就是說,首先應從parent ClassLoader中加載。如果parent ClassLoader中沒有,再從子ClassLoader加載。值得注意的是,由於JVM是採用類名+ClassLoader實例進行加載的,因此加載時不採用上述順序也是可以的。這樣可能會造成多個不同的ClassLoader都加載了某Class,這樣使用時可能會帶來ClassCastException異常。因此在加載類順序上需合理把握,儘量保證從根到最下層的ClassLoader上的Class只加載了一次。

ClassLoader抽象類提供了幾個關鍵的方法用於繼承:

loadClass

此方法負責加載指定名字的類,ClassLoader的實現方法爲:先從已經加載的類中尋找,如沒有,則繼續從parent ClassLoader中尋找;如果仍然沒找到,則從System ClassLoader中尋找,最後在調用findClass方法來尋找。如果要改變類的加載順序,則可覆蓋此方法;如果加載順序相同,則可以通過覆蓋findClass來做特殊處理,例如解密,尋找固定路徑等。當通過整個尋找類的過程仍然未獲取Class對象時,則拋出ClassNotFoundException。如果類需要resolve,則調用resolveClass進行鏈接。

findLoadedClass

此方法負責從當前ClassLoader實例對象的緩存中尋找已經加載的類,調用的爲native的方法。

findClass

此方法直接拋出ClassNotFoundException,因此要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。

findSystemClass

此方法負責從SystemClassLoader中尋找類,如未找到,則繼續從Bootstrap ClassLoader中尋找,如果仍然未找到,則返回null。

defineClass

此方法負責將二進制字節碼轉換爲Class對象。如果二進制字節碼不符合JVM Class文件格式,則拋出ClassFormatError;如果生成的類名和二進制字節碼中的不同,則拋出NoClassDefFoundError;如果加載的類是受保護的,採用不同簽名的,或者類名是以java.開頭的,則拋出SecurityException;如果加載的class在此ClassLoader中已加載,則拋出LinkageError。

resolveClass

此方法完成Class對象的鏈接,如果鏈接過,則直接返回。

當Java開發人員調用Class.forName來獲取一個對應名稱的class對象時,JVM會從方法棧上尋找第一個ClassLoader,並使用此ClassLoader來加載。JVM爲了保護加載,執行的類的安全,不允許ClassLoader直接卸載加載了的類。只有JVM才能卸載。在Sun JDK中,只有當ClassLoader對象沒有引用時,此CLassLoader對象加載的類纔會被卸載。

類加載方面常見的異常有哪些呢?

ClassNotFoundException

原因是當前ClassLoader加載類時沒有找到相應的類文件。這裏要注意自定義ClassLoader。如果有的話需要具體看一下加載過程。

NoClassDefFoundError

造成此異常的主要原因是加載的類中引用到的另外的類不存在。因此,對於這個異常,需要先看看是加載哪個類報出的,然後再確定該類中引用的類是否存在於當前ClassLoader能加載到的位置。

LinkageError

該異常在自定義ClassLoader的情況下更容易出現,主要原因是此類已經在ClassLoader加載過了,重複的加載會造成該異常,因此要避免在併發的情況下出現這樣的問題。由於JVM這個保護機制,使得JVM中沒辦法直接更新一個已經load的Class,只能創建一個新的ClassLoader來加載更新的class,然後將新的請求轉入該ClassLoader中來獲取類。其他更多的原因是對象狀態的膚質,依賴的設置等。

ClassCastException

該異常由多種原因,在JVM支持範型以後,合理使用範型會減少此異常的觸發。這些原因中比較難查的是兩個A對象由不同的ClassLoader加載的情況,如果一個對象當另一個不同ClassLoader加載的相同對象使用,也會報ClassCastException。

發佈了45 篇原創文章 · 獲贊 19 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章