你了解 Java 的類加載器嗎?類加載機制是什么?什么是雙親委派機制?
什么是類加載器,類加載器有哪些?
實現(xiàn)通過類的全限定名獲取該類的二進制字節(jié)流的代碼塊叫做類加載器。
主要有一下四種類加載器:
- 啟動類加載器:用來加載 Java 核心類庫,無法被 Java 程序直接引用。
- 擴展類加載器:它用來加載 Java 的擴展庫。Java 虛擬機的實現(xiàn)會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。
- 系統(tǒng)類加載器:它根據(jù)應(yīng)用的類路徑來加載 Java 類??赏ㄟ^ClassLoader.getSystemClassLoader() 獲取它。
- 自定義類加載器:通過繼承java.lang.ClassLoader 類的方式實現(xiàn)。
JVM類加載機制?
Java 的類加載器機制與雙親委派模型是 Java 虛擬機(JVM)加載類文件時采用的一種體系結(jié)構(gòu)。它用于確保 Java 應(yīng)用程序中類的單一性、安全性和加載順序。
- 全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
- 緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效
- 雙親委派機制:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
什么是雙親委派機制?
一個類加載器收到一個類的加載請求時,它首先不會自己嘗試去加載它,而是把這個請求委派給父類加載器去完成,這樣層層委派,因此所有的加載請求最終都會傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。
圖片
雙親委派模型的具體實現(xiàn)代碼在 java.lang.ClassLoader 中,此類的 loadClass() 方法運行過程如下:先檢查類是否已經(jīng)加載過,如果沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出ClassNotFoundException ,此時嘗試自己去加載。
雙親委派模型目的?
可以防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼。如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個 java.lang.Object 的同名類并放在 ClassPath 中,多個類加載器都去加載這個類到內(nèi)存中,系統(tǒng)中將會出現(xiàn)多個不同的 Object 類,那么類之間的比較結(jié)果及類的唯一性將無法保證。
什么時候需要打破雙親委派模型?
比如類A已經(jīng)有一個classA,恰好類B也有一個clasA 但是兩者內(nèi)容不一致,如果不打破雙親委派模型,那么類A只會加載一次
只要在加載類的時候,不按照UserCLASSlOADER->Application ClassLoader->Extension ClassLoader->Bootstrap ClassLoader的順序來加載就算打破打破雙親委派模型了。比如自定義個ClassLoader,重寫loadClass方法(不依照往上開始尋找類加載器),那就算是打破雙親委派機制了。
打破雙親委派模型的方式?
有兩種方式:
- 自定義一個類加載器的類,并覆蓋抽象類java.lang.ClassL oader中l(wèi)oadClass..)方法,不再優(yōu)先委派“父”加載器進行類加載。(比如Tomcat)
- 主動違背類加載器的依賴傳遞原則
例如在一個BootstrapClassLoader加載的類中,又通過APPClassLoader來加載所依賴的其它類,這就打破了“雙親委派模型”中的層次結(jié)構(gòu),逆轉(zhuǎn)了類之間的可見性。
典型的是Java SPI機制,它在類ServiceLoader中,會使用線程上下文類加載器來逆向加載classpath中的第三方廠商提供的Service Provider類。(比如JDBC)
什么是依賴傳遞原則?
如果一個類由類加載器A加載,那么這個類的依賴類也是由「相同的類加載器」加載。
Tomcat是如何打破雙親委派模型的?
在Tomcat部署項目時,是把war包放到tomcat的webapp下,這就意味著一個tomcat可以運行多個Web應(yīng)用程序。
假設(shè)現(xiàn)在有兩個Web應(yīng)用程序,它們都有一個類,叫User,并且它們的類全限定名都一樣,比如都是com.yyy.User,但是他們的具體實現(xiàn)是不一樣的。那么Tomcat如何保證它們不會沖突呢?
Tomcat給每個 Web 應(yīng)用創(chuàng)建一個類加載器實例(WebAppClassLoader),該加載器重寫了loadClass方法,優(yōu)先加載當前應(yīng)用目錄下的類,如果當前找不到了,才一層一層往上找,這樣就做到了Web應(yīng)用層級的隔離。
但是并不是Web應(yīng)用程序的所有依賴都需要隔離的,比如要用到Redis的話,Redis就可以再Web應(yīng)用程序之間貢獻,沒必要每個Web應(yīng)用程序每個都獨自加載一份。因此Tomcat就在WebAppClassLoader上加個父加載器ShareClassLoader,如果WebAppClassLoader沒有加載到這個類,就委托給ShareClassLoader去加載。(意思就類似于將需要共享的類放到一個共享目錄下)
Web應(yīng)用程序有類,但是Tomcat本身也有自己的類,為了隔絕這兩個類,就用CatalinaClassLoader類加載器進行隔離,CatalinaClassLoader加載Tomcat本身的類
Tomcat與Web應(yīng)用程序還有類需要共享,那就再用CommonClassLoader作為CatalinaClassLoader和ShareClassLoader的父類加載器,來加載他們之間的共享類
Tomcat加載結(jié)構(gòu)圖如下:
圖片
JDBC 是如何打破雙親委派模型的?
實際上JDBC定義了接口,具體的實現(xiàn)類是由各個廠商進行實現(xiàn)的(比如MySQL)
類加載有個規(guī)則:如果一個類由類加載器A加載,那么這個類的依賴類也是由「相同的類加載器」加載。
而在用JDBC的時候,是使用DriverManager獲取Connection的,DriverManager是在java.sql包下的,顯然是由BootStrap類加載器進行裝載的。當使用DriverManager.getConnection ()時,需要得到的一定是對應(yīng)廠商(如Mysql)實現(xiàn)的類。這里在去獲取Connection的時候,是使用「線程上下文加載器」去加載Connection的,線程上下文加載器會直接指定對應(yīng)的加載器去加載。也就是說,在BootStrap類加載器利用「線程上下文加載器」指定了對應(yīng)的類的加載器去加載
圖片
什么線程上下文加載器?
Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現(xiàn)。常見的 SPI 有 JDBC 。
這些 SPI 的接口由 Java 核心庫來提供,而這些 SPI 的實現(xiàn)代碼則是作為 Java 應(yīng)用所依賴的 jar 包被包含進類路徑(CLASSPATH)里。SPI接口中的代碼經(jīng)常需要加載具體的實現(xiàn)類。那么問題來了,SPI的接口是Java核心庫的一部分,是由啟動類加載器來加載的;SPI的實現(xiàn)類是由系統(tǒng)類加載器來加載的。啟動類加載器是無法找到 SPI 的實現(xiàn)類的,因為它只加載 Java 的核心庫。它也不能委派給系統(tǒng)類加載器,因為它是系統(tǒng)類加載器的祖先類加載器。
線程上下文類加載器正好解決了這個問題。如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現(xiàn)的類。線程上下文類加載器在很多 SPI 的實現(xiàn)中都會用到。
線程上下文加載器的一般使用模式(獲取 - 使用 - 還原)。
ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
try {
//設(shè)置線程上下文類加載器為自定義的加載器
Thread.currentThread.setContextClassLoader(targetTccl);
myMethod(); //執(zhí)行自定義的方法
} finally {
//還原線程上下文類加載器
Thread.currentThread().setContextClassLoader(classLoader);
}
能自定義類加載器加載 java.lang.String嗎?
很多人都有個誤區(qū):雙親委派機制不能被打破,不能使用自定義類加載器加載java.lang.String
但是事實上并不是,只要重寫ClassLoader的loadClass()方法,就能打破了。
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
publicclass MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls) {
super(urls);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//只對MyClassLoader和String使用自定義的加載,其他的還是走雙親委派
if(name.equals("MyClassLoader") || name.equals("java.lang.String")) {
returnsuper.findClass(name);
} else {
return getParent().loadClass(name);
}
}
public static void main(String[] args) throws Exception {
//urls指定自定義類加載器的加載路徑
URL url = new File("J:/apps/demo/target/classes/").toURI().toURL();
URL url3 = new File("C:/Program Files/Java/jdk1.8.0_191/jre/lib/rt.jar").toURI().toURL();
URL[] urls = {
url
, url3
};
MyClassLoader myClassLoader = new MyClassLoader(urls);
Class<?> c1 = MyClassLoader.class.getClassLoader().loadClass("MyClassLoader");
Class<?> c2 = myClassLoader.loadClass("MyClassLoader");
System.out.println(c1 == c2); //false
System.out.println(c1.getClassLoader()); //AppClassLoader
System.out.println(c2.getClassLoader()); //MyClassLoader
System.out.println(myClassLoader.loadClass("java.lang.String")); //Exception
}
}
加載同一個類MyClassLoader,使用的類加載器不同,說明這里是打破了雙親委派機制的,但是嘗試加載String類的時候報錯了
圖片
看代碼是ClassLoader類里面的限制,只要加載java開頭的包就會報錯。所以真正原因是JVM安全機制,并不是因為雙親委派。
那么既然是ClassLoader里面的代碼做的限制,那把ClassLoader.class修改了不就好了嗎。
寫了個java.lang.ClassLoader,把preDefineClass()方法里那段if直接刪掉,再用編譯后的class替換rt.jar里面的,直接通過命令jar uvf rt.jar java/lang/ClassLoader/class即可。
不過事與愿違,修改之后還是報錯:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at com.example.demo.mini.test.MyClassLoader.loadClass(MyClassLoader.java:17)
at com.example.demo.mini.test.MyClassLoader.main(MyClassLoader.java:31)
仔細看報錯和之前的不一樣了,這次是native方法報錯了。這就比較難整了,看來要自己重新編譯個JVM才行了。理論上來說,編譯JVM的時候把校驗的代碼去掉就行了。
結(jié)論:自定義類加載器加載java.lang.String,必須修改jdk的源碼,自己重新編譯個JVM才行。