JVM 类加载器与双亲委派模型
1. 類加載器
我們知道,虛擬機在加載類的過程中需要使用類加載器進行加載,而在 Java 中,類加載器有很多,那么當 JVM 想要加載一個 .class 文件的時候,到底應該由哪個類加載器加載呢?這時候就需要雙親委派機制來告訴 JVM 使用哪個類加載器加載。在講解什么是雙親委派機制之前,我們先看一下有哪些加載器。
從 Java 虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器 Bootstrap ClassLoader,這個類加載器使用 C++ 語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由 Java 語言實現,獨立于虛擬機外部,并且全都繼承自抽象類 java.lang.ClassLoader。從 Java 開發人員的角度來看,類加載器還可以劃分得更細致一些,分為用戶級別和系統級別類加載器。用戶級別的類加載器我們統一稱為自定義類加載器,而系統級別的類加載器有:
- 啟動類加載器:Bootstrap ClassLoader
- 擴展類加載器:Extention ClassLoader
- 應用程序類加載器:Application ClassLoader
1.1 啟動類加載器
啟動類加載器 Bootstrap ClassLoader 使用 C/C++ 語言實現,負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用 null 代替即可。
可以通過如下代碼查看啟動類加載器可以加載哪些路徑的 jar:
String bootStrapPath = System.getProperty("sun.boot.class.path"); System.out.println("啟動類加載器加載的路徑: "); for (String paths : bootStrapPath.split(";")){for (String path : paths.split(":")) {System.out.println(path);} }1.2 擴展類加載器
擴展類加載器 Extension ClassLoader 由 Java 語言編寫,并由 sun.misc.Launcher$ExtClassLoader 實現,父類加載器為啟動類加載器。負責加載 <JAVA_HOME>\lib\ext 目錄中的,或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫。開發者可以直接使用擴展類加載器,如果用戶創建的 JAR 放在擴展目錄下,也會自動由擴展類加載器加載。
可以通過如下代碼查看擴展類加載器可以加載哪些路徑的 jar:
String extClassLoaderPath = System.getProperty("java.ext.dirs"); System.out.println("拓展類加載器加載的路徑: "); for (String paths : extClassLoaderPath.split(";")){for (String path : paths.split(":")) {System.out.println(path);} }1.3 應用程序類加載器
應用程序類加載器 Application ClassLoader 由 Java 語言編寫,并由 sun.misc.Launcher$App-ClassLoader 實現,父類加載器為擴展類加載器。由于這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑 ClassPath 或系統屬性 java.class.path 指定路徑下的類庫。開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
可以通過如下代碼查看應用程序類加載器可以加載哪些路徑的 jar:
String appClassLoaderPath = System.getProperty("java.class.path"); for (String paths : appClassLoaderPath.split(";")){System.out.println("應用程序類加載器加載的路徑: ");for (String path : paths.split(":")) {System.out.println(path);} }1.4 自定義類加載器
在 Java 的日常應用程序開發中,類的加載幾乎是由上述 3 種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。那么什么場景下需要自定義類加載器呢?
- 隔離加載類
- 修改類加載的方式
- 擴展加載源
- 防止源碼泄漏
開發人員可以通過繼承抽象類 java.lang.ClassLoader 類的方式,實現自己的類加載器,以滿足一些特殊的需求。在 JDK 1.2 之前,在自定義類加載器時,總會去繼承 ClassLoader 類并重寫 loadClass() 方法,從而實現自定義的類加載類,但是在 JDK 1.2 之后已不再建議用戶去覆蓋 loadClass() 方法,而是建議把自定義的類加載邏輯寫在 findclass() 方法中。下面我們來實現一個自定義類加載器并演示如何使用。第一步自定義一個實體類 Car.java:
// 測試對象 Car public class Car {public Car() {System.out.println("welcome you");}public void print() {System.out.println("this is a car");} }第二步自定義一個類加載器,我們定義的 CustomClassLoader 繼承自 java.lang.ClassLoader,且只實現 findClass 方法:
// 自定義加載器 public class CustomClassLoader extends ClassLoader{private String path;public CustomClassLoader(String path) {this.path = path;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {System.out.println("CustomClassLoader: " + name);try {String fileName = path + "/" + name.substring(name.lastIndexOf(".") + 1) + ".class";FileInputStream inputStream = new FileInputStream(fileName);if (inputStream == null) {return super.findClass(name);}byte[] bytes = new byte[inputStream.available()];inputStream.read(bytes);return defineClass(name, bytes, 0, bytes.length);} catch (IOException ex) {throw new ClassNotFoundException(name, ex);}} }第三步演示自定義類加載器如何使用:
CustomClassLoader myClassLoader = new CustomClassLoader("/opt/data"); Class<?> myClass = myClassLoader.loadClass("com.common.example.bean.Car"); // 創建對象實例 Object o = myClass.newInstance(); // 調用方法 Method print = myClass.getDeclaredMethod("print", null); print.invoke(o, null); // 輸出類加載器 System.out.println("ClassLoader: " + o.getClass().getClassLoader());直接運行上述代碼,會輸出如下結果:
welcome you this is a car ClassLoader: sun.misc.Launcher$AppClassLoader@49476842從上面看到輸出結果并不符合我們的預期,Car 類使用的應用程序類加載器加載的,并不是我們自定義的類加載器。這個問題主要是因為 Idea 編譯后會存放在 target/classes 目錄下
而這個目錄正好是應用程序類加載的路徑,可以使用ClassLoaderPathExample代碼驗證。為了解決這個問題,我們可以把 Car.class 手動移動到 /opt/data 目錄下(刪除 target/classes 目錄下的 Car.class 文件,避免由應用程序類加載器加載)。再次運行輸出如下結果:
CustomClassLoader: com.common.example.bean.Car welcome you this is a car ClassLoader: com.common.example.jvm.classLoader.CustomClassLoader@4617c264這樣 Car 類就使用我們自定義的類加載器加載了。
2. 什么是雙親委派模型
上述四種類加載器之間存在著一種層次關系,如下圖所示:
一般認為上一層加載器是下一層加載器的父類加載器,除了啟動類加載器 BootstrapClassLoader 之外,所有的加載器都是有父類加載器。我們可以先通過如下代碼來看一下類加載器的層級結構:
// 應用程序類加載器(系統類加載器) ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@49476842// 獲取上層加載器:擴展類加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@5acf9800// 獲取上層加載器:啟動類加載器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // null在上述代碼中依次輸出當前類的類加載器,父類加載器以及父類的父類加載器??梢钥吹疆斍邦惖募虞d器是應用程序類加載器,它的父類親加載器是擴展類加載器,擴展類加載器的父類輸出了一個 null,這個 null 會去調用啟動類加載器。后續通過 ClassLoader 類的源碼我們可以知道這一點。
那到底什么是雙親委派模型呢?其實我們把上述類加載器之間的這種層次關系,我們稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系一般不會以繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來復用父加載器的代碼。
類加載器的雙親委派模型是在 JDK 1.2 期間被引入并被廣泛應用于之后幾乎所有的 Java 程序中。但它并不是一個強制性的約束模型,而是 Java 設計者推薦給開發者的一種類加載器實現方式。
我們從概念上知道了什么是雙親委派模型,那它到底是如何工作的呢?雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都委派到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
3. 為什么需要雙親委派模型
如上面我們提到的,因為類加載器之間有嚴格的層次關系,那么也就使得 Java 類也隨之具備了一種帶有優先級的層次關系。例如類 java.lang.Object,它存放在 rt.jar 之中,無論哪一個類加載器要加載這個類,但最終都委派給最頂層的啟動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為 java.lang.Object 的類,并放在程序的 ClassPath 中,那系統中將會出現多個不同的 Object 類,Java 類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂。
通過上面我們可以知道雙親委派模型的核心是保障類加載的唯一性和安全性:
- 唯一性:可以避免類的重復加載,當父類加載器已經加載過某一個類時,子加載器就不會再重新加載這個類。例如上述提及的 java.lang.Object 類,最終都委派給最頂層的啟動類加載器進行加載,因此 Object 類在程序的各種類加載器環境中都是同一個類。
- 安全性:保證了 Java 的核心 API 不被篡改。因為啟動類加載器 Bootstrap ClassLoader 在加載的時候,只會加載 JAVA_HOME 中的 jar 包里面的類,如 java.lang.Object,那么就可以避免加載自定義的有破壞能力的 java.lang.Object。
4. 雙親委派模型是怎么實現的
雙親委派模型對于保證 Java 程序的穩定運作很重要,但它的實現卻非常簡單,實現雙親委派的代碼都集中在 java.lang.ClassLoader 的 loadClass() 方法之中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先檢查類是否已經被加載過Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 若沒有加載過并且有父類加載器則調用父類加載器的 loadClass() 方法c = parent.loadClass(name, false);} else {// 調用啟動類加載器c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {// 如果仍未找到,則調用 findClass 以查找該類。long t1 = System.nanoTime();c = findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;} }首先檢查類是否已經被加載過,若沒有加載過并且有父類加載器則調用父類加載器的 loadClass() 方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,拋出 ClassNotFoundException 異常后,再調用自己的 findClass() 方法進行加載。
5. 如何破壞雙親委派模型
雙親委派模型并不是一個強制性的約束模型,而是 Java 設計者推薦給開發者的類加載器實現方式。在 Java 的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現過 3 較大規模的’被破壞’情況。
5.1 JDK 1.2 歷史原因
雙親委派模型的第一次’被破壞’其實發生在雙親委派模型出現之前,即 JDK 1.2 發布之前。由于雙親委派模型在 JDK 1.2 之后才被引入,而類加載器和抽象類 java.lang.ClassLoader 則在 JDK 1.0 時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java 設計者引入雙親委派模型時不得不做出一些妥協。為了向前兼容,JDK 1.2 之后的 java.lang.ClassLoader 添加了一個新的 protected 方法 findClass(),在此之前,用戶去繼承 java. lang.ClassLoader 的唯一目的就是為了重寫 loadClass() 方法,因為虛擬機在進行類加載的時候會調用加載器的私有方法 loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的 loadClass()。上一節我們已經看過 loadClass() 方法的代碼,雙親委派的具體邏輯就實現在這個方法之中,JDK 1.2之 后已不提倡用戶再去覆蓋 loadClass() 方法,而應當把自己的類加載邏輯寫到 findClass() 方法中,在 loadClass() 方法的邏輯里如果父類加載失敗,則會調用自己的 findClass() 方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規則的。
5.2 SPI
雙親委派模型的第二次’被破壞’是由這個模型自身的缺陷所導致的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱為“基礎”,是因為它們總是作為被用戶代碼調用的 API,但世事往往沒有絕對的完美,如果基礎類又要調用回用戶的代碼,那該怎么辦?
這并非是不可能的事情,一個典型的例子便是 JNDI 服務,JNDI 現在已經是 Java 的標準服務,它的代碼由啟動類加載器去加載(在 JDK 1.3 時放進去的 rt.jar 中),但 JNDI 的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現并部署在應用程序 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代碼,但啟動類加載器不可能’認識’這些代碼。為了解決這個問題,Java 設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoaser() 方法進行設置,如果創建線程時還未設置,將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。有了線程上下文類加載器,就可以做一些’舞弊’的事情了,JNDI 服務使用這個線程上下文類加載器去加載所需要的 SPI 代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java 中所有涉及 SPI 的加載動作基本上都采用這種方式,例如 JNDI、JDBC、JCE、JAXB和JBI等。
5.3 模塊化
雙親委派模型的第三次’被破壞’是由于用戶對程序動態性的追求而導致的,這里所說的’動態性’指的是當前一些非?!療衢T’的名詞:代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等。Sun 公司所提出的JSR-294、JSR-277 規范在與 JCP 組織的模塊化規范之爭中落敗給 JSR-291(即OSGi R4.2),雖然 Sun 不甘失去 Java 模塊化的主導權,獨立在發展 Jigsaw 項目,但目前 OSGi 已經成為了業界事實上的 Java 模塊化標準,而 OSGi 實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊(OSGi 中稱為 Bundle)都有一個自己的類加載器,當需要更換一個 Bundle 時,就把 Bundle 連同類加載器一起換掉以實現代碼的熱替換。在 OSGi 環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構。
參考:
- 深入理解 Java 虛擬機
- 我竟然被“雙親委派”給虐了
總結
以上是生活随笔為你收集整理的JVM 类加载器与双亲委派模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 移动安全-Frida脱壳脚本与加固迭代
- 下一篇: 抖音高贵气质的签名_抖音上很火的个性签名