JVM要雙親委派的原因及如何打破它
一、類加載器
類加載器,顧名思義就是一個(gè)可以將Java字節(jié)碼加載為java.lang.Class實(shí)例的工具。這個(gè)過(guò)程包括,讀取字節(jié)數(shù)組、驗(yàn)證、解析、初始化等。另外,它也可以加載資源,包括圖像文件和配置文件。
類加載器的特點(diǎn):
- 動(dòng)態(tài)加載,無(wú)需在程序一開(kāi)始運(yùn)行的時(shí)候加載,而是在程序運(yùn)行的過(guò)程中,動(dòng)態(tài)按需加載,字節(jié)碼的來(lái)源也很多,壓縮包jar、war中,網(wǎng)絡(luò)中,本地文件等。類加載器動(dòng)態(tài)加載的特點(diǎn)為熱部署,熱加載做了有力支持。
- 全盤負(fù)責(zé),當(dāng)一個(gè)類加載器加載一個(gè)類時(shí),這個(gè)類所依賴的、引用的其他所有類都由這個(gè)類加載器加載,除非在程序中顯式地指定另外一個(gè)類加載器加載。所以破壞雙親委派不能破壞擴(kuò)展類加載器以上的順序。
一個(gè)類的唯一性由加載它的類加載器和這個(gè)類的本身決定(類的全限定名+類加載器的實(shí)例ID作為唯一標(biāo)識(shí))。比較兩個(gè)類是否相等(包括Class對(duì)象的equals()、isAssignableFrom()、isInstance()以及instanceof關(guān)鍵字等),只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來(lái)源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,這兩個(gè)類就必定不相等。
從實(shí)現(xiàn)方式上,類加載器可以分為兩種:一種是啟動(dòng)類加載器,由C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;另一種是繼承于java.lang.ClassLoader的類加載器,包括擴(kuò)展類加載器、應(yīng)用程序類加載器以及自定義類加載器。
啟動(dòng)類加載器(Bootstrap ClassLoader):負(fù)責(zé)加載<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如rt.jar,名字不符合的類庫(kù)即使放在lib目錄中也不會(huì)被加載)類庫(kù)加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載器無(wú)法被Java程序直接引用,用戶在編寫(xiě)自定義類加載器時(shí),如果想設(shè)置Bootstrap ClassLoader為其parent,可直接設(shè)置null。
擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定路徑中的所有類庫(kù)。該類加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。擴(kuò)展類加載器由啟動(dòng)類加載器加載,其父類加載器為啟動(dòng)類加載器,即parent=null。
應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù),由sun.misc.Launcher$App-ClassLoader實(shí)現(xiàn)。開(kāi)發(fā)者可直接通過(guò)java.lang.ClassLoader中的getSystemClassLoader()方法獲取應(yīng)用程序類加載器,所以也可稱它為系統(tǒng)類加載器。應(yīng)用程序類加載器也是啟動(dòng)類加載器加載的,但是它的父類加載器是擴(kuò)展類加載器。在一個(gè)應(yīng)用程序中,系統(tǒng)類加載器一般是默認(rèn)類加載器。
二、雙親委派機(jī)制
2.1 什么是雙親委派
JVM 并不是在啟動(dòng)時(shí)就把所有的.class文件都加載一遍,而是程序在運(yùn)行過(guò)程中用到了這個(gè)類才去加載。除了啟動(dòng)類加載器外,其他所有類加載器都需要繼承抽象類ClassLoader,這個(gè)抽象類中定義了三個(gè)關(guān)鍵方法,理解清楚它們的作用和關(guān)系非常重要。
public abstract class ClassLoader {
//每個(gè)類加載器都有個(gè)父加載器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下這個(gè)類是不是已經(jīng)加載過(guò)了
Class<?> c = findLoadedClass(name);
//如果沒(méi)有加載過(guò)
if( c == null ){
//先委派給父加載器去加載,注意這是個(gè)遞歸調(diào)用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加載器為空,查找Bootstrap加載器是不是加載過(guò)了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加載器沒(méi)加載成功,調(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對(duì)象
return defineClass(buf, off, len);
}
// 將字節(jié)碼數(shù)組解析成一個(gè)Class對(duì)象,用native方法實(shí)現(xiàn)
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
從上面的代碼可以得到幾個(gè)關(guān)鍵信息:
- JVM 的類加載器是分層次的,它們有父子關(guān)系,而這個(gè)關(guān)系不是繼承維護(hù),而是組合,每個(gè)類加載器都持有一個(gè)
parent字段,指向父加載器。(AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null。) defineClass方法的職責(zé)是調(diào)用 native 方法把 Java 類的字節(jié)碼解析成一個(gè) Class 對(duì)象。findClass方法的主要職責(zé)就是找到.class文件并把.class文件讀到內(nèi)存得到字節(jié)碼數(shù)組,然后調(diào)用defineClass方法得到 Class 對(duì)象。子類必須實(shí)現(xiàn)findClass。loadClass方法的主要職責(zé)就是實(shí)現(xiàn)雙親委派機(jī)制:首先檢查這個(gè)類是不是已經(jīng)被加載過(guò)了,如果加載過(guò)了直接返回,否則委派給父加載器加載,這是一個(gè)遞歸調(diào)用,一層一層向上委派,最頂層的類加載器(啟動(dòng)類加載器)無(wú)法加載該類時(shí),再一層一層向下委派給子類加載器加載。

2.2 為什么要雙親委派?
雙親委派保證類加載器,自下而上的委派,又自上而下的加載,保證每一個(gè)類在各個(gè)類加載器中都是同一個(gè)類。
一個(gè)非常明顯的目的就是保證java官方的類庫(kù)<JAVA_HOME>\lib和擴(kuò)展類庫(kù)<JAVA_HOME>\lib\ext的加載安全性,不會(huì)被開(kāi)發(fā)者覆蓋。
例如類java.lang.Object,它存放在rt.jar之中,無(wú)論哪個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。
如果開(kāi)發(fā)者自己開(kāi)發(fā)開(kāi)源框架,也可以自定義類加載器,利用雙親委派模型,保護(hù)自己框架需要加載的類不被應(yīng)用程序覆蓋。
三、破壞雙親委派
如果想自定義類加載器,就需要繼承ClassLoader,并重寫(xiě)findClass,如果想不遵循雙親委派的類加載順序,還需要重寫(xiě)loadClass。如下是一個(gè)自定義的類加載器,并重寫(xiě)了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ū)建立一個(gè)java.lang.Class對(duì)象的實(shí)例
// 用來(lái)封裝 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);
}
}

測(cè)試加載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());
}
}
//控制臺(tái)打印
TestClassLoader的父類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
Demo的類加載器:com.stefan.DailyTest.classLoader.TestClassLoader@78308db1
注意破壞雙親委派的位置,自定義類加載機(jī)制先委派給ExtClassLoader加載,ExtClassLoader再委派給BootstrapClassLoader,如果都加載不了,然后自定義類加載器加載,自定義類加載器加載不了才交給AppClassLoader。為什么不能直接讓自定義類加載器加載呢?
不能!雙親委派的破壞只能發(fā)生在AppClassLoader及其以下的加載委派順序,ExtClassLoader上面的雙親委派是不能破壞的!
因?yàn)槿魏晤惗际抢^承自超類java.lang.Object,而加載一個(gè)類時(shí),也會(huì)加載繼承的類,如果該類中還引用了其他類,則按需加載,且類加載器都是加載當(dāng)前類的類加載器。
如Demo類只隱式繼承了Object,自定義類加載器TestClassLoader加載了Demo,也會(huì)加載Object。如果loadClass直接調(diào)用TestClassLoader的findClass會(huì)報(bào)錯(cuò)java.lang.SecurityException: Prohibited package name: java.lang。
為了安全,java是不允許除BootStrapClassLOader以外的類加載器加載官方java.目錄下的類庫(kù)的。在defineClass源碼中,最終會(huì)調(diào)用native方法defineClass1獲取Class對(duì)象,在這之前會(huì)檢查類的全限定名name是否是java.開(kāi)頭。(如果想完全繞開(kāi)java的類加載,需要自己實(shí)現(xiàn)defineClass,但是因?yàn)閭€(gè)人能力有限,沒(méi)有深入研究defineClass的重寫(xiě),并且一般情況也不會(huì)破壞ExtClassLoader以上的雙親委派,除非不用java了。)


通過(guò)自定義類加載器破壞雙親委派的案例在日常開(kāi)發(fā)中非常常見(jiàn),比如Tomcat為了實(shí)現(xiàn)web應(yīng)用間加載隔離,自定義了類加載器,每個(gè)Context代表一個(gè)web應(yīng)用,都有一個(gè)webappClassLoader。再如熱部署、熱加載的實(shí)現(xiàn)都是需要自定義類加載器的。破壞的位置都是跳過(guò)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);
}
五、線程上下文類加載器
線程上下文類加載器其實(shí)是一種類加載器傳遞機(jī)制??梢酝ㄟ^(guò)java.lang.Thread#setContextClassLoader方法給一個(gè)線程設(shè)置上下文類加載器,在該線程后續(xù)執(zhí)行過(guò)程中就能把這個(gè)類加載器?。?code>java.lang.Thread#getContextClassLoader)出來(lái)使用。
如果創(chuàng)建線程時(shí)未設(shè)置上下文類加載器,將會(huì)從父線程(parent = currentThread())中獲取,如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置過(guò),就默認(rèn)是應(yīng)用程序類加載器。
線程上下文類加載器的出現(xiàn)就是為了方便破壞雙親委派:
一個(gè)典型的例子便是JNDI服務(wù),JNDI現(xiàn)在已經(jīng)是Java的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動(dòng)類加載器去加載(在JDK 1.3時(shí)放進(jìn)去的rt.jar),但JNDI的目的就是對(duì)資源進(jìn)行集中管理和查找,它需要調(diào)用由獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啟動(dòng)類加載器不可能去加載ClassPath下的類。
但是有了線程上下文類加載器就好辦了,JNDI服務(wù)使用線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請(qǐng)求子類加載器去完成類加載的動(dòng)作,這種行為實(shí)際上就是打通了雙親委派模型的層次結(jié)構(gòu)來(lái)逆向使用類加載器,實(shí)際上已經(jīng)違背了雙親委派模型的一般性原則,但這也是無(wú)可奈何的事情。
Java中所有涉及SPI的加載動(dòng)作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
摘自《深入理解java虛擬機(jī)》周志明
六、要點(diǎn)回顧
1.java 的類加載,就是獲取.class文件的二進(jìn)制字節(jié)碼數(shù)組并加載到 JVM 的方法區(qū),并在 JVM 的堆區(qū)建立一個(gè)用來(lái)封裝 java 類相關(guān)的數(shù)據(jù)和方法的java.lang.Class對(duì)象實(shí)例。
2.java默認(rèn)有的類加載器有三個(gè),啟動(dòng)類加載器(BootstrapClassLoader),擴(kuò)展類加載器(ExtClassLoader),應(yīng)用程序類加載器(也叫系統(tǒng)類加載器)(AppClassLoader)。類加載器之間存在父子關(guān)系,這種關(guān)系不是繼承關(guān)系,是組合關(guān)系。如果parent=null,則它的父級(jí)就是啟動(dòng)類加載器。啟動(dòng)類加載器無(wú)法被java程序直接引用。
3.雙親委派就是類加載器之間的層級(jí)關(guān)系,加載類的過(guò)程是一個(gè)遞歸調(diào)用的過(guò)程,首先一層一層向上委托父類加載器加載,直到到達(dá)最頂層啟動(dòng)類加載器,啟動(dòng)類加載器無(wú)法加載時(shí),再一層一層向下委托給子類加載器加載。
4.雙親委派的目的主要是為了保證java官方的類庫(kù)<JAVA_HOME>\lib和擴(kuò)展類庫(kù)<JAVA_HOME>\lib\ext的加載安全性,不會(huì)被開(kāi)發(fā)者覆蓋。
5.破壞雙親委派有兩種方式:第一種,自定義類加載器,必須重寫(xiě)findClass和loadClass;第二種是通過(guò)線程上下文類加載器的傳遞性,讓父類加載器中調(diào)用子類加載器的加載動(dòng)作。
參考:
- 《深入理解java虛擬機(jī)》周志明(書(shū)中對(duì)類加載的介紹非常詳盡,部分精簡(jiǎn)整理后引用。)
- 《深入拆解Tomcat & Jetty》Tomcat如何打破雙親委托機(jī)制?
- 李號(hào)雙《Tomcat內(nèi)核設(shè)計(jì)剖析》汪建,第十三章 公共與隔離的類加載
到此這篇關(guān)于JVM要雙親委派的原因及如何打破它的文章就介紹到這了,更多相關(guān)JVM雙親委派內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
groovy腳本定義結(jié)構(gòu)表一鍵生成POJO類
這篇文章主要為大家介紹了groovy腳本定義結(jié)構(gòu)表一鍵生成POJO類示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
基于Spring中的線程池和定時(shí)任務(wù)功能解析
下面小編就為大家?guī)?lái)一篇基于Spring中的線程池和定時(shí)任務(wù)功能解析。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
Java實(shí)現(xiàn)開(kāi)箱即用的redis分布式鎖
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)開(kāi)箱即用的基于redis的分布式鎖,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以收藏一下2022-12-12
Springboot如何優(yōu)雅地進(jìn)行字段校驗(yàn)
這篇文章主要給大家介紹了關(guān)于Springboot如何優(yōu)雅地進(jìn)行字段校驗(yàn)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Java利用反射自動(dòng)封裝成實(shí)體對(duì)象的方法
這篇文章主要介紹了Java利用反射自動(dòng)封裝成實(shí)體對(duì)象的方法,可實(shí)現(xiàn)自動(dòng)封裝成bean對(duì)象功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
Java8函數(shù)式編程應(yīng)用小結(jié)
Java8非常重要的就是引入了函數(shù)式編程的思想,使得這門經(jīng)典的面向?qū)ο笳Z(yǔ)言有了函數(shù)式的編程方式,彌補(bǔ)了很大程度上的不足,函數(shù)式思想在處理復(fù)雜問(wèn)題上有著更為令人稱贊的特性,本文給大家介紹Java8函數(shù)式編程應(yīng)用小結(jié),感興趣的朋友一起看看吧2023-12-12
Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析
這篇文章主要介紹了Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12

