【JAVA核心知識】5: JVM的類加載

1 類加載過程

想要使用一個類,首先需要將其加載到JVM中,類加載到JVM需要經過三個步驟:加載->鏈接->初始化。其中鏈接又分爲驗證,準備,解析三步。
在這裏插入圖片描述

1.1 加載

類加載階段會在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各個數據的入口。注意相關信息不是一定要從Class文件中獲取,它即可與從ZIP中讀取(如從Jar包,War包中讀取),也可以在運算時生成(動態代理),也可以由其它文件生成(如將JSP文件轉換成對應的Class類)。

1.2 鏈接

鏈接過程分爲驗證,準備,解析三步。

1.2.1 驗證

JAVA是一種相對安全的語言,驗證的意義是爲了確保Class文件的字節流中包含的信息符合當前虛擬機要求。不會危害到虛擬機的安全。驗證主要包含文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

  • 文件格式驗證:驗證字節流是否符合Class文件格式規範,並且能被當前JVM加載處理。如常量類型是否支持。
  • 元數據驗證:字節碼信息進行語言分析,分析是否符合java語言規範。
  • 字節碼驗證:最重要的驗證環節,元數據驗證後對方法體驗證,保證類方法在運行時不會又危害發生。
  • 符號引用驗證:驗證符號引用,保證能夠訪問到,不會出現無法訪問的情況。

1.2.2 準備

準備階段正式對類變量分配內存,並設置初始默認值。如定義 private static int s = 2020;經過此階段s=0,而不是2020.但是如果定義爲private final static int s = 2020,在準備階段會直接將s設置成2020而不是初始值0;

1.2.3 解析

解析階段JVM將常量池中的符號引用替換爲直接引用。準備階段只是分配了內存,但是類變量並沒有指向那一塊內存,這一步就是完成實際指向的工作。

1.3 初始化

初始化階段爲類變量設置正確的初始值。如上文 private static int s = 2020中將s賦值2020的動作遍在這一步完成。同時初始化階段也會執行靜態代碼塊。如果有超類,則先對超類執行初始化。
初始化階段是執行類構造器<client>方法的過程,<client>方法是編譯器自動收集類中的類變量賦值操作和靜態語句塊中的語句合併而成的。JVM會保證父類的<client>方法會在子類的<client>方法執行之前執行完畢。如果這個類沒有靜態變量賦值和靜態代碼塊,那麼編譯器可以不爲這個類生成<client>方法。
以下幾種情況不會觸發初始化:

  • 子類引用父類的靜態變量,只會觸發父類的初始化,不會觸發子類的初始化
  • 定義對象數組,不會觸發初始化
  • 常量在編譯過程中直接存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類的初始化。如A引用B類的常量final static x = 3。此時不會對B進行初始化。
  • 通過類名獲取Class對象,不會觸發類的初始化。
  • 通過Class.forName加載指定類時,如果initalize爲false,不會觸發初始化。
  • 通過ClassLoader默認的loadClss方法,也不會觸發初始化。

2 類加載器

類加載的動作由類加載器完成。對於JVM來說,類的唯一性是通過類的全限定名+類加載器來區分的。不同的類加載器加載的同一個類並不被認爲是同一類。如有一個類C,分別用類加載器CL1,CL2加載。一個參數需要CL1的C實例,傳入的確實CL2的C實例,就會報錯java.lang.ClassCastException:C cannot be cast to C。
JVM提供了三種類加載器:啓動類加載器(Bootstrap ClassLoader),擴展類加載器(Extension ClassLoader),應用程序類加載器(Application ClassLoader).

  • 啓動類加載器:用來加載Java的核心庫,主要加載的是JVM自身所需要的類,使用C++實現,並非繼承於java.lang.ClassLoader,是JVM的一部分。負責加載JAVA_HOME\lib目錄中的,或者-Xbootclasspath參數指定的路徑中的,且被虛擬機認可[注1]的類。開發者無法直接獲取到其引用。
    注1:JVM是按文件名識別的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包放在lib目錄下也沒有作用,同時啓動加載器只加載包名爲java,javax,sun等開頭的類。且java是特殊包名,開發者的包名不能以java開頭,如果自定義了一個java.***包來讓類加載器加載,那麼就會拋出異常java.lang.SecurityException: Prohibited package name: java.***
  • 擴展類加載器: 用來加載Java的擴展庫。負責加載JAVA_HOME\lib\ext目錄中的,或通過系統變量java.ext.dirs指定路徑中的類庫。由java語言實現。開發者可以直接使用。
  • 應用程序類加載器:負責加載用戶路徑(classpath)上的類庫。開發者可以直接使用。可以通過ClassLoader.getSystemClassLoader()獲得。一般情況下程序的默認類加載器就是該加載器。

除了提供的加載器外,開發者可以通過繼承ClassLoader類的方式實現自己的類加載器。
類加載器

3 雙親委派機制

JVM的類加載機制爲雙親委派機制,除了頂層的啓動類加載器,雙親委派機制要求每一個類加載器都要有自己的父加載器。這個父加載器並不是指繼承,而是一種委派關係。雙親委派機制下一個類加載器收到類加載請求不會直接自己去加載,而是先把這個請求委託給自己的父類加載器去執行,如果父類加載器還存在父類加載器,則繼續委託,一直委託到最頂層的啓動類加載器。父加載器可以加載目標類的話就由父加載器完成加載任務,如果父加載器無法完成則子加載器自己嘗試加載。這就是雙親委派機制。
雙親委派機制的優勢:雙親委派機制使得java類隨着他的類加載器具備了一種帶有帶有優先級的層級關係。通過這種層級關係可以避免類的的重複加載,當父加載器已經加載過目標類時,子加載器無需重複加載一次。
其次是安全方面,通過雙親委派機制可以避免核心類不會被隨意替換,例如網絡傳遞一個名爲java.lang.Integer的類,在雙親委派機制下,加載請求會被傳遞到頂層的啓動類加載器,啓動類加載器發現這個名字的類已經加載過了,就會直接返回已經加載過的Integer.class。這樣就可以防止核心API被修改。

4 OSGI

OSGI(Open Service Gateway Initiative)是面向java的動態模型系統。OSGI能夠提供無需重啓的動態改造功能,基於OSGI的程序很可能可以實現模塊級的熱插拔功能,當程序進行升級時,只需停用,重新安裝然後啓動程序的一部分。但並非所有的程序都適合OSGI架構,其在提供強大功能的同時也提高了複雜度,因爲OSGI不支持雙親委派機制。
eclipse就是基於OSGI技術來構建的。
OSGI每個模塊都自己的類,每個模塊可以聲明其需要模塊的類(導入),也可以聲明自己的類以供其它模塊使用(導出),每個模塊都有自己的類加載器,他負責加載本模塊的類,對於非本模塊的類:核心庫的類會代理給父類加載器(通常是啓動類加載器),其他模塊導入的類則代理給對應模塊由其加載。對於java開頭的類,默認都是由父類加載器完成的,可以通過org.osgi.framework.bootdelegation設置某些包或者類必須由父類加載器加載。如設置org.osgi.framework.bootdelegation = com.my.* 此時com.my下的所有類都由父類加載器進行加載
例如由兩個模塊:M-A和M-B,分別有類C-A和C-B,C-A繼承自C-B,M-A啓動時,對C-A進行加載,因爲繼承關係繼而需要加載C-B,此時由於M-A聲明瞭C-B是由M-B導入的,那麼就會將C-B的加載交由M-B執行,M-B對C-B進行加載,所得的類實例可以被所有聲明導入此類的模塊使用。


參考資料:
jvm之java類加載機制和類加載器(ClassLoader)的詳解
深入探討 Java 類加載器

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