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