學(xué)網(wǎng)絡(luò)推廣哪個培訓(xùn)機構(gòu)好windows優(yōu)化大師官方免費下載
文章目錄
- 1、類加載機制
- 2、雙親委派模型
- 2.1、介紹
- 2.2、為什么需要雙親委派
- 2.3、源碼解析
- 3、破壞雙親委派
- 3.1、介紹
- 3.2、破壞實現(xiàn)
- 3.3、破壞雙親委派的例子
- 4、線程上下文類加載器
1、類加載機制
類加載階段分為加載、連接、初始化三個階段,而加載階段需要通過類的全限定名來獲取定義了此類的二進制字節(jié)流。Java特意把這一步抽出來用類加載器來實現(xiàn)。把這一步驟抽離出來使得應(yīng)用程序可以按需自定義類加載器。并且得益于類加載器,OSGI、熱部署等領(lǐng)域才得以在JAVA中得到應(yīng)用。
這一部分可參考之前的文章《【JVM】搞清類加載機制》。
在Java中任意一個類都是由這個類本身和加載這個類的類加載器來確定這個類在JVM中的唯一性。也就是你用A類加載器加載的com.aa.ClassA
和B類加載器加載的com.aa.ClassA
它們是不同的,也就是用instanceof
這種對比都是不同的,所以即使都來自于同一個class文件但是由不同類加載器加載的那就是兩個獨立的類。
站在Java虛擬機的角度來看,只存在兩種不同的類加載器,分別是啟動類加載器和其它所有的類加載器。其中啟動類加載器使用C++
語言實現(xiàn),是虛擬機自身的一部分;其它所有的類加載器則都是由Java
語言實現(xiàn),是獨立于虛擬機外部,并且全部都繼承自抽象類java.lang.ClassLoader
。
站在Java開發(fā)人員的角度來看,類加載器就應(yīng)該劃分得更細(xì)致一些。自JDK1.2
以來,Java一直保持著三層類加載器、雙親委派的類加載結(jié)構(gòu)。其中JDK提供的三層類加載器如下:
- 啟動類加載器(Boostrap Class Loader):C++語言實現(xiàn),是虛擬機自身的一部分,負(fù)責(zé)加載存放在
<JAVA_HOME>\lib
目錄,或者被-Xbootclasspath
參數(shù)所指定的路徑中存放的,而且是Java虛擬機能夠識別的。啟動類加載器無法被Java程序直接引用,在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器時,直接使用null代替即可;
注意的設(shè)計,Java虛擬機是按照文件名識別的,如rt.jar、tools.jar,名字不符合的類庫及時放在lib目錄中也不會被加載。
-
擴展類加載器(Extension Class Loader):獨立于虛擬機外部,于類
sun.misc.launcher$ExtClassLoader
中以Java代碼的形式實現(xiàn),負(fù)責(zé)加載<JAVA_HOME>\lib\ext
目錄,或者被java.ext.dirs
系統(tǒng)變量所指定的路徑中所有的類庫; -
應(yīng)用程序類加載器(Application Class Loader):獨立于虛擬機外部,由
sun.misc.launcher$AppClassLoader
以Java代碼的形式實現(xiàn),負(fù)責(zé)加載**用戶類路徑(ClassPath
)**上所有的類庫,在開發(fā)過程中可直接使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下就是這個程序中默認(rèn)的類加載器。
2、雙親委派模型
2.1、介紹
介紹完JDK中的三種類加載器,那么就不得不說一下雙親委派模型了。
JDK9之前的Java應(yīng)用都是由上述三種類加載器互相配合來完成加載的,如果覺得有必要,還可自定義類加載器來對功能進行拓展。
雙親委派模型(Parents Delegation Medel)是指各種類加載器之間的層級關(guān)系。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)有自己的父類加載器。同時這里類加載器之間的父子關(guān)系一般是通過組合關(guān)系來復(fù)用父加載器的代碼而非繼承。
雙親委派模型核心的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳遞到最頂層的啟動類加載器中。只有當(dāng)父加載器反饋自己無法完成這個加載請求,即搜索范圍內(nèi)沒有找到所需要的類時,子加載器才會嘗試自己去完成加載。
2.2、為什么需要雙親委派
雙親委派保證類加載器,自下而上的委派,又自上而下的加載,保證每一個類在各個類加載器中都是同一個類。
使用雙親委派模型來組織類加載器之前的關(guān)系,一個顯而易見的好處就是Java中的類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層級關(guān)系。
一個非常明顯的目的就是保證Java官方的類庫<JAVA_HOME>\lib
和擴展類庫<JAVA_HOME>\lib\ext
的加載安全性,不會被開發(fā)者覆蓋。
例如類java.lang.Object
,它存放在rt.jar
之中,無論哪個類加載器要加載這個類,最終都是委派給啟動類加載器加載,從而保證Object類在程序的各種類加載器環(huán)境中從始至終都是同一個類。
如果我們自己開發(fā)開源框架,也可以自定義類加載器,利用雙親委派模型,保護自己框架需要加載的類不被應(yīng)用程序覆蓋。
2.3、源碼解析
雙親委派模型對于保證Java程序的穩(wěn)定運作極為重要,但它的實現(xiàn)卻異常簡單,用以實現(xiàn)雙親委派的代碼只有短短十余行,全部集中在java.lang.ClassLoader
的loadClass()
方法之中:
public abstract class ClassLoader {//每個類加載器都有個父加載器private final ClassLoader parent;/*** 雙親委派核心實現(xiàn)*/public Class<?> loadClass(String name) {//查找一下這個類是不是已經(jīng)加載過了Class<?> c = findLoadedClass(name);//如果沒有加載過if( c == null ){//先委派給父加載器去加載,注意這是個遞歸調(diào)用if (parent != null) {c = parent.loadClass(name);}else {// 如果父加載器為空,查找Bootstrap加載器是不是加載過了c = findBootstrapClassOrNull(name);}}// 如果父加載器沒加載成功,調(diào)用自己的findClass去加載if (c == null) {c = findClass(name);}return c;}protected Class<?> findClass(String name){//1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內(nèi)存...//2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對象return defineClass(buf, off, len);}// 將字節(jié)碼數(shù)組解析成一個Class對象,用native方法實現(xiàn)protected final Class<?> defineClass(byte[] b, int off, int len){...}
}
從上面的代碼可以得到幾個關(guān)鍵信息:
- JVM 的類加載器是分層次的,它們有父子關(guān)系,而這個關(guān)系不是繼承維護,而是組合,每個類加載器都持有一個 parent 字段,指向父加載器。
AppClassLoader
的parent
是ExtClassLoader
,ExtClassLoader
的parent
是BootstrapClassLoader
,但是BootstrapClassLoader
的parent=null
。
-
defineClass
方法的職責(zé)是調(diào)用native
方法把 Java 類的字節(jié)碼解析成一個 Class 對象。 -
findClass
方法的主要職責(zé)就是找到.class
文件并把.class
文件讀到內(nèi)存得到字節(jié)碼數(shù)組,然后調(diào)用defineClass
方法得到 Class 對象。子類必須實現(xiàn)findClass
。 -
loadClass
方法的主要職責(zé)就是實現(xiàn)雙親委派機制:首先檢查這個類是否已經(jīng)被加載過了,如果加載過了直接返回,否則委派給父加載器加載,這是一個遞歸調(diào)用,一層一層向上委派,最頂層的類加載器(啟動類加載器)無法加載該類時,再一層一層向下委派給子類加載器加載。
3、破壞雙親委派
3.1、介紹
雙親委派模型對于保證Java程序的穩(wěn)定運作極為重要,但它并不是一個具有強制性約束力的模型,而是Java設(shè)計者推薦給開發(fā)者們的類加載器實現(xiàn)方式,這就代表著雙親委派機制是可以被破壞的。
3.2、破壞實現(xiàn)
前面提及到我們是可以自定義類加載器的,實現(xiàn)的方法也很簡單,繼承ClassLoader
類并重寫相關(guān)的方法即可。而當(dāng)我們想要破壞雙親委派計智時,我們也需要自定義一個類加載器,通過實現(xiàn)自己的類加載邏輯打破原有的加載順序。
在這里對ClassLoader
類的三個核心方法進行介紹,在前面的源碼解析中也有提及:
loadClass()
:主要進行類加載的方法,默認(rèn)的雙親委派機制就實現(xiàn)在這個方法中;findClass()
:根據(jù)名稱或位置加載.class字節(jié)碼;definclass()
:把字節(jié)碼轉(zhuǎn)化為Class。
接下來將舉一個簡單的例子,當(dāng)只有加載String時才會委派給父加載器進行加載,否則通過自定義的類加載邏輯進行加載,從而破壞雙親委派機制。具體實現(xiàn)如下:
public class MyClassLoader extends ClassLoader {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {if (!"java.lang.String".equals(name)) {// 如果類名不是java.lang.String,直接加載類return findClass(name);} else {// 如果類名是java.lang.String,委派給父ClassLoader加載return super.loadClass(name);}}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 實現(xiàn)自己的類加載邏輯// ...}
}
需要注意的是,破壞雙親委派機制可能會導(dǎo)致類加載的混亂和不穩(wěn)定性,不推薦在正式的應(yīng)用程序中使用。雙親委派機制的目的是為了保證類的加載是有序的、穩(wěn)定的,能夠避免類的重復(fù)加載和沖突。在一些特殊的情況下,可能需要破壞雙親委派機制來實現(xiàn)一些特定的需求,但在大多數(shù)情況下,最好遵循雙親委派機制的原則。
3.3、破壞雙親委派的例子
? 破壞雙親委派的例子有許多,最容易想到的便是自己搞破壞了,還有許多我們是不知道的,如雙親委派機制歷史中的三次較大規(guī)?!氨黄茐摹钡那闆r、JDBC、OSGI等模塊化技術(shù)、Tomcat等。
這里簡單說一下Tomcat是如何破壞雙親委派機制的。
在Tomcat中,每個Web應(yīng)用程序都有自己的類加載器,稱為Web應(yīng)用程序類加載器(WebAppClassLoader
)。當(dāng)Tomcat啟動時,會為每個Web應(yīng)用程序創(chuàng)建一個獨立的WebAppClassLoader,它負(fù)責(zé)加載該Web應(yīng)用程序的類。
為了保證每個Web項目互相獨立,所以不能都由AppClassLoader加載,因此自定義了類加載器WebappClassLoader
,WebappClassLoader
繼承自URLClassLoader
,重寫了findClass和loadClass
,并且WebappClassLoader
的父類加載器設(shè)置為AppClassLoader
。
WebappClassLoader
加載過程如下:
WebappClassLoader.loadClass
中會先在緩存中查看類是否加載過;- 如果已經(jīng)加載過,直接返回;
- 如果沒有加載過,就交給
ExtClassLoader
,ExtClassLoader
再交給BootstrapClassLoader
加載; - 如果都加載不了,才自己加載;
- 如果自己也加載不了,就遵循原始的雙親委派,交由AppClassLoader遞歸加載。
4、線程上下文類加載器
線程上下文類加載器其實是一種類加載器傳遞機制??梢酝ㄟ^java.lang.Thread#setContextClassLoader
方法給一個線程設(shè)置上下文類加載器,在該線程后續(xù)執(zhí)行過程中就能通過java.lang.Thread#getContextClassLoader
把這個類加載器取出來使用。
如果創(chuàng)建線程時未設(shè)置上下文類加載器,將會從父線程(parent = currentThread()
)中獲取,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,就默認(rèn)是應(yīng)用程序類加載器。
一個典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動類加載器去加載(在JDK 1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調(diào)用由獨立廠商實現(xiàn)并部署在應(yīng)用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啟動類加載器不可能去加載ClassPath下的類。
但是有了線程上下文類加載器就好辦了,JNDI服務(wù)使用線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打破了雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,實際上已經(jīng)違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。
Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
參考:
- 《深入理解Java虛擬機-JVM高級特性與實踐》
- 你確定你真的理解"雙親委派"了嗎?
- 《深入拆解Tomcat & Jetty》——Tomcat如何打破雙親委托機制?
- 【JVM】搞清類加載機制