JVM要雙親委派的原因及如何打破它
一、類加載器
類加載器,顧名思義就是一個可以將Java字節(jié)碼加載為java.lang.Class
實例的工具。這個過程包括,讀取字節(jié)數(shù)組、驗證、解析、初始化等。另外,它也可以加載資源,包括圖像文件和配置文件。
類加載器的特點:
- 動態(tài)加載,無需在程序一開始運行的時候加載,而是在程序運行的過程中,動態(tài)按需加載,字節(jié)碼的來源也很多,壓縮包jar、war中,網(wǎng)絡(luò)中,本地文件等。類加載器動態(tài)加載的特點為熱部署,熱加載做了有力支持。
- 全盤負(fù)責(zé),當(dāng)一個類加載器加載一個類時,這個類所依賴的、引用的其他所有類都由這個類加載器加載,除非在程序中顯式地指定另外一個類加載器加載。所以破壞雙親委派不能破壞擴(kuò)展類加載器以上的順序。
一個類的唯一性由加載它的類加載器和這個類的本身決定(類的全限定名+類加載器的實例ID作為唯一標(biāo)識)。比較兩個類是否相等(包括Class對象的equals()
、isAssignableFrom()
、isInstance()
以及instanceof
關(guān)鍵字等),只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機(jī)加載,只要加載它們的類加載器不同,這兩個類就必定不相等。
從實現(xiàn)方式上,類加載器可以分為兩種:一種是啟動類加載器,由C++語言實現(xiàn),是虛擬機(jī)自身的一部分;另一種是繼承于java.lang.ClassLoader
的類加載器,包括擴(kuò)展類加載器、應(yīng)用程序類加載器以及自定義類加載器。
啟動類加載器(Bootstrap ClassLoader
):負(fù)責(zé)加載<JAVA_HOME>\lib
目錄中的,或者被-Xbootclasspath
參數(shù)所指定的路徑,并且是虛擬機(jī)識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果想設(shè)置Bootstrap ClassLoader
為其parent
,可直接設(shè)置null。
擴(kuò)展類加載器(Extension ClassLoader
):負(fù)責(zé)加載<JAVA_HOME>\lib\ext
目錄中的,或者被java.ext.dirs
系統(tǒng)變量所指定路徑中的所有類庫。該類加載器由sun.misc.Launcher$ExtClassLoader
實現(xiàn)。擴(kuò)展類加載器由啟動類加載器加載,其父類加載器為啟動類加載器,即parent=null
。
應(yīng)用程序類加載器(Application ClassLoader
):負(fù)責(zé)加載用戶類路徑(ClassPath
)上所指定的類庫,由sun.misc.Launcher$App-ClassLoader
實現(xiàn)。開發(fā)者可直接通過java.lang.ClassLoader
中的getSystemClassLoader()
方法獲取應(yīng)用程序類加載器,所以也可稱它為系統(tǒng)類加載器。應(yīng)用程序類加載器也是啟動類加載器加載的,但是它的父類加載器是擴(kuò)展類加載器。在一個應(yīng)用程序中,系統(tǒng)類加載器一般是默認(rèn)類加載器。
二、雙親委派機(jī)制
2.1 什么是雙親委派
JVM 并不是在啟動時就把所有的.class
文件都加載一遍,而是程序在運行過程中用到了這個類才去加載。除了啟動類加載器外,其他所有類加載器都需要繼承抽象類ClassLoader
,這個抽象類中定義了三個關(guān)鍵方法,理解清楚它們的作用和關(guān)系非常重要。
public abstract class ClassLoader { //每個類加載器都有個父加載器 private final ClassLoader parent; 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)系不是繼承維護(hù),而是組合,每個類加載器都持有一個
parent
字段,指向父加載器。(AppClassLoader
的parent
是ExtClassLoader
,ExtClassLoader
的parent
是BootstrapClassLoader
,但是ExtClassLoader
的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ī)制:首先檢查這個類是不是已經(jīng)被加載過了,如果加載過了直接返回,否則委派給父加載器加載,這是一個遞歸調(diào)用,一層一層向上委派,最頂層的類加載器(啟動類加載器)無法加載該類時,再一層一層向下委派給子類加載器加載。
2.2 為什么要雙親委派?
雙親委派保證類加載器,自下而上的委派,又自上而下的加載,保證每一個類在各個類加載器中都是同一個類。
一個非常明顯的目的就是保證java
官方的類庫<JAVA_HOME>\lib
和擴(kuò)展類庫<JAVA_HOME>\lib\ext
的加載安全性,不會被開發(fā)者覆蓋。
例如類java.lang.Object
,它存放在rt.jar
之中,無論哪個類加載器要加載這個類,最終都是委派給啟動類加載器加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。
如果開發(fā)者自己開發(fā)開源框架,也可以自定義類加載器,利用雙親委派模型,保護(hù)自己框架需要加載的類不被應(yīng)用程序覆蓋。
三、破壞雙親委派
如果想自定義類加載器,就需要繼承ClassLoader
,并重寫findClass
,如果想不遵循雙親委派的類加載順序,還需要重寫loadClass
。如下是一個自定義的類加載器,并重寫了loadClass
破壞雙親委派:
package com.stefan.DailyTest.classLoader; import java.io.*; public class TestClassLoader extends ClassLoader { public TestClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 1、獲取class文件二進(jìn)制字節(jié)數(shù)組 byte[] data = null; try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileInputStream fis = new FileInputStream(new File("C:\\study\\myStudy\\JavaLearning\\target\\classes\\com\\stefan\\DailyTest\\classLoader\\Demo.class")); byte[] bytes = new byte[1024]; int len = 0; while ((len = fis.read(bytes)) != -1) { baos.write(bytes, 0, len); } data = baos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } // 2、字節(jié)碼數(shù)組加載到 JVM 的方法區(qū), // 并在 JVM 的堆區(qū)建立一個java.lang.Class對象的實例 // 用來封裝 Java 類相關(guān)的數(shù)據(jù)和方法 return this.defineClass(name, data, 0, data.length); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException{ // 1、找到ext classLoader,并首先委派給它加載,為什么? ClassLoader classLoader = getSystemClassLoader(); while (classLoader.getParent() != null) { classLoader = classLoader.getParent(); } Class<?> clazz = null; try { clazz = classLoader.loadClass(name); } catch (ClassNotFoundException e) { // Ignore } if (clazz != null) { return clazz; } // 2、自己加載 clazz = this.findClass(name); if (clazz != null) { return clazz; } // 3、自己加載不了,再調(diào)用父類loadClass,保持雙親委派模式 return super.loadClass(name); } }
測試加載Demo類:
package com.stefan.DailyTest.classLoader; public class Test { public static void main(String[] args) throws Exception { // 初始化TestClassLoader,并將加載TestClassLoader類的類加載器 // 設(shè)置為TestClassLoader的parent TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader()); System.out.println("TestClassLoader的父類加載器:" + testClassLoader.getParent()); // 加載 Demo Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo"); System.out.println("Demo的類加載器:" + clazz.getClassLoader()); } } //控制臺打印 TestClassLoader的父類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2 Demo的類加載器:com.stefan.DailyTest.classLoader.TestClassLoader@78308db1
注意破壞雙親委派的位置,自定義類加載機(jī)制先委派給ExtClassLoader
加載,ExtClassLoader
再委派給BootstrapClassLoader
,如果都加載不了,然后自定義類加載器加載,自定義類加載器加載不了才交給AppClassLoader
。為什么不能直接讓自定義類加載器加載呢?
不能!雙親委派的破壞只能發(fā)生在AppClassLoader
及其以下的加載委派順序,ExtClassLoader
上面的雙親委派是不能破壞的!
因為任何類都是繼承自超類java.lang.Object
,而加載一個類時,也會加載繼承的類,如果該類中還引用了其他類,則按需加載,且類加載器都是加載當(dāng)前類的類加載器。
如Demo
類只隱式繼承了Object
,自定義類加載器TestClassLoader
加載了Demo
,也會加載Object
。如果loadClass
直接調(diào)用TestClassLoader
的findClass
會報錯java.lang.SecurityException: Prohibited package name: java.lang
。
為了安全,java
是不允許除BootStrapClassLOader
以外的類加載器加載官方java.
目錄下的類庫的。在defineClass
源碼中,最終會調(diào)用native
方法defineClass1
獲取Class對象,在這之前會檢查類的全限定名name
是否是java.
開頭。(如果想完全繞開java的類加載,需要自己實現(xiàn)defineClass
,但是因為個人能力有限,沒有深入研究defineClass
的重寫,并且一般情況也不會破壞ExtClassLoader
以上的雙親委派,除非不用java了。)
通過自定義類加載器破壞雙親委派的案例在日常開發(fā)中非常常見,比如Tomcat為了實現(xiàn)web應(yīng)用間加載隔離,自定義了類加載器,每個Context
代表一個web應(yīng)用,都有一個webappClassLoader
。再如熱部署、熱加載的實現(xiàn)都是需要自定義類加載器的。破壞的位置都是跳過AppClassLoader
。
四、Class.forName默認(rèn)使用的類加載器
1. forName(String name, boolean initialize,ClassLoader loader)
可以指定classLoader
。
2.不顯式傳classLoader
就是默認(rèn)當(dāng)前類的類加載器:
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
五、線程上下文類加載器
線程上下文類加載器其實是一種類加載器傳遞機(jī)制??梢酝ㄟ^java.lang.Thread#setContextClassLoader
方法給一個線程設(shè)置上下文類加載器,在該線程后續(xù)執(zhí)行過程中就能把這個類加載器?。?code>java.lang.Thread#getContextClassLoader)出來使用。
如果創(chuàng)建線程時未設(shè)置上下文類加載器,將會從父線程(parent = currentThread()
)中獲取,如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,就默認(rèn)是應(yīng)用程序類加載器。
線程上下文類加載器的出現(xiàn)就是為了方便破壞雙親委派:
一個典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動類加載器去加載(在JDK 1.3時放進(jìn)去的rt.jar),但JNDI的目的就是對資源進(jìn)行集中管理和查找,它需要調(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虛擬機(jī)》周志明
六、要點回顧
1.java 的類加載,就是獲取.class
文件的二進(jìn)制字節(jié)碼數(shù)組并加載到 JVM 的方法區(qū),并在 JVM 的堆區(qū)建立一個用來封裝 java 類相關(guān)的數(shù)據(jù)和方法的java.lang.Class
對象實例。
2.java默認(rèn)有的類加載器有三個,啟動類加載器(BootstrapClassLoader),擴(kuò)展類加載器(ExtClassLoader),應(yīng)用程序類加載器(也叫系統(tǒng)類加載器)(AppClassLoader)。類加載器之間存在父子關(guān)系,這種關(guān)系不是繼承關(guān)系,是組合關(guān)系。如果parent=null
,則它的父級就是啟動類加載器。啟動類加載器無法被java程序直接引用。
3.雙親委派就是類加載器之間的層級關(guān)系,加載類的過程是一個遞歸調(diào)用的過程,首先一層一層向上委托父類加載器加載,直到到達(dá)最頂層啟動類加載器,啟動類加載器無法加載時,再一層一層向下委托給子類加載器加載。
4.雙親委派的目的主要是為了保證java
官方的類庫<JAVA_HOME>\lib
和擴(kuò)展類庫<JAVA_HOME>\lib\ext
的加載安全性,不會被開發(fā)者覆蓋。
5.破壞雙親委派有兩種方式:第一種,自定義類加載器,必須重寫findClass
和loadClass
;第二種是通過線程上下文類加載器的傳遞性,讓父類加載器中調(diào)用子類加載器的加載動作。
參考:
- 《深入理解java虛擬機(jī)》周志明(書中對類加載的介紹非常詳盡,部分精簡整理后引用。)
- 《深入拆解Tomcat & Jetty》Tomcat如何打破雙親委托機(jī)制?
- 李號雙《Tomcat內(nèi)核設(shè)計剖析》汪建,第十三章 公共與隔離的類加載
到此這篇關(guān)于JVM要雙親委派的原因及如何打破它的文章就介紹到這了,更多相關(guān)JVM雙親委派內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
groovy腳本定義結(jié)構(gòu)表一鍵生成POJO類
這篇文章主要為大家介紹了groovy腳本定義結(jié)構(gòu)表一鍵生成POJO類示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03Springboot如何優(yōu)雅地進(jìn)行字段校驗
這篇文章主要給大家介紹了關(guān)于Springboot如何優(yōu)雅地進(jìn)行字段校驗的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Java8函數(shù)式編程應(yīng)用小結(jié)
Java8非常重要的就是引入了函數(shù)式編程的思想,使得這門經(jīng)典的面向?qū)ο笳Z言有了函數(shù)式的編程方式,彌補(bǔ)了很大程度上的不足,函數(shù)式思想在處理復(fù)雜問題上有著更為令人稱贊的特性,本文給大家介紹Java8函數(shù)式編程應(yīng)用小結(jié),感興趣的朋友一起看看吧2023-12-12