要說雙親委派機制,還得從類加載器的型別談起
一、類加載器的型別
類加載器有以下種類:
- 啟動類加載器(Bootstrap ClassLoader)
- 擴展類加載器(Extension ClassLoader)
- 應用類加載器(Application ClassLoader)
啟動類加載器
內嵌在JVM內核中的加載器,由C++語言撰寫(因此也不會繼承ClassLoader),是類加載器層次中最頂層的加載器,用于加載java的核心類別庫,即加載jre/lib/rt.jar里所有的class,由于啟動類加載器涉及到虛擬機本地實作細節,我們無法獲取啟動類加載器的參考,
擴展類加載器
它負責加載JRE的擴展目錄,jre/lib/ext或者由java.ext.dirs系統屬性指定的目錄中jar包的類,父類加載器為啟動類加載器,但使用擴展類加載器呼叫getParent依然為null,
應用類加載器
又稱系統類加載器,可用通過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類加載器的實體,系統類加載器也因此得名,應用類加載器主要加載classpath下的class,即用戶自己撰寫的應用編譯得來的class,呼叫getParent回傳擴展類加載器,
擴展類加載器與應用類加載器繼承結構如圖所示:

可以看到除了啟動類加載器,其余的兩個類加載器都繼承于ClassLoader,我們自定義的類加載器,也需要繼承ClassLoader,
值得注意的是,啟動類、擴展類與應用類加載器之間的父子關系,并不是通過繼承來實作的,而是通過組合,即使用parent變數來保存“父加載器”的參考,
二、雙親委派機制
當一個類加載器收到了一個類加載請求時,它自己不會先去嘗試加載這個類,而是把這個請求轉交給父類加載器,每一個層的類加載器都是如此,因此所有的類加載請求都應該傳遞到最頂層的啟動類加載器中,只有當父類加載器在自己的加載范圍內沒有搜尋到該類時,并向子類反饋自己無法加載后,子類加載器才會嘗試自己去加載,
加載標準類別庫與用戶代碼,會有不同的方式:

ClassLoader內的loadClass方法,就很好的解釋了雙親委派的加載程序:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//檢查該class是否已經被當前類加載器加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
//此時該class還沒有被加載
try {
if (parent != null) {
//如果父加載器不為null,則委托給父類加載
c = parent.loadClass(name, false);
} else {
//如果父加載器為null,說明當前類加載器已經是啟動類加載器,直接時候用啟動類加載器去加載該class
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//此時父類加載器都無法加載該class,則使用當前類加載器進行加載
long t1 = System.nanoTime();
c = findClass(name);
...
}
}
//是否需要連接該類
if (resolve) {
resolveClass(c);
}
return c;
}
}
三、雙親委派存在的意義
為什么要使用雙親委派機制呢?
假設用戶自己定義了java.lang.Object類,由于雙親委派機制的存在,最侄訓委托到啟動類加載器去加載,即回傳rt.jar中的Object類,并不會加載用戶撰寫的Object類,
大家上班摸魚刷的LeetCode,本質上自定義了一個類加載器,重寫了findClass方法,會從網路中加載位元組碼,生成Class物件,最終通過loadClass定義的雙親委派機制進行加載,如果這個時候,我定義了一個惡意java.lang.Object類,在沒有雙親委派機制的情況下,可能會對jvm產生安全風險,
雙親委派機制存在的意義,就是為了防止findClass與defineclass生成的Class物件覆寫掉標準類別庫中的基礎類,避免產生安全風險,
四、如何自定義類加載器
我們整理ClassLoader里面的流程
- loadclass:雙親委派機制,子加載器委托父加載器加載,父加載器都加載失敗時,子加載器通過findclass自行加載
- findclass:當前類加載器根據路徑以及class檔案名稱加載位元組碼,從class檔案中讀取位元組陣列,然后使用defineClass
- defineclass:根據位元組陣列,回傳Class物件
我們在ClassLoader里面找到findClass方法,發現該方法直接拋出例外,應該是留給子類實作的,
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
到這里,我們應該明白,loadClass方法使用了模版方法模式,主線邏輯是雙親委派,但如何將class檔案轉化為Class物件的步驟,已經交由子類去實作,對模版方法模式不熟悉的同學,可以先參考我的另外一篇文章模版方法模式
其實原始碼中,已經有一個自定義類加載的樣例代碼,在注釋中:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
}
}
看得出來,如果我們需要自定義類加載器,只需要繼承ClassLoader,并且重寫findClass方法即可,
現在有一個簡單的樣例,class檔案依然在檔案目錄中:
package com.yang.testClassLoader;
import sun.misc.Launcher;
import java.io.*;
public class MyClassLoader extends ClassLoader {
/**
* 類加載路徑,不包含檔案名
*/
private String path;
public MyClassLoader(String path) {
super();
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = getBytesFromClass(name);
assert bytes != null;
//讀取位元組陣列,轉化為Class物件
return defineClass(name, bytes, 0, bytes.length);
}
//讀取class檔案,轉化為位元組陣列
private byte[] getBytesFromClass(String name) {
String absolutePath = path + "/" + name + ".class";
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(new File(absolutePath));
bos = new ByteArrayOutputStream();
byte[] temp = new byte[1024];
int len;
while ((len = fis.read(temp)) != -1) {
bos.write(temp, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bos) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader = new MyClassLoader("C://develop");
Class test = classLoader.loadClass("Student");
test.newInstance();
}
}
Student類:
public class Student {
public Student() {
System.out.println("student classloader is" + this.getClass().getClassLoader().toString());
}
}
注意,這個Student類千萬不要加包名,idea報錯不管他即可,然后使用javac Student.java編譯該類,將生成的class檔案復制到c://develop下即可,
運行MyClassLoader的main方法后,可以看到輸出:

看得出來,Student.class確實是被我們自定義的類加載器給加載了,
五、雙親委派機制能被破壞嗎
從上面的自定義類加載器的內容中,我們應該可以猜到了,破壞雙親委派直接重寫loadClass方法就完事了,事實上,我們確實可以重寫loadClass方法,畢竟這個方法沒有被final修飾,雙親委派既然有好處,為什么jdk對loadClass開放重寫呢?這要從雙親委派引入的時間來看:
雙親委派模型是在JDK1.2之后才被引入的,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實作代碼,Java設計者引入雙親委派模型時不得不做出一些妥協,在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,jdk為了向前兼容,不得已開放對loadClass的重寫操作,
當然,破壞也不止這一次,jdbc與tomcat也破壞了雙親委派,
六、JDBC對雙親委派的破壞
還記得,我們第一次學jdbc的時候,是怎么連接資料庫的嗎?
先參考一個mysql-connector-java的jar包,這里的版本是5.0.8
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.8</version>
</dependency>
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
這段代碼,真的是勾起了我好多回憶啊~ 想起了那年在夕陽下奔跑的時光,那是我逝去的青春
首先要說明的是,該版本的jdbc并沒有去打破雙親委派,或者說jdbc4.0前沒有破壞雙親委派,
資料庫這么多,jdk為了統一管理資料庫驅動,在java.sql下定義了Driver介面,具體的實作由資料庫廠商去做,
mysql對Driver介面的實作類是com.mysql.jdbc.Driver類,位于我們新引入的jar包中,
我們進入Class.forName中,發現最侄訓使用應用類加載器去加載com.mysql.jdbc.Driver類,
而該Driver位于引入的jar包中,確實是應該被應用類加載器加載,

接著進入到com.mysql.jdbc包下的Driver類中,它實作了rt.jar中的java.sql.Driver介面,
Class.forName會初始化該類,初始化的時候會執行靜態方法,
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
//將mysql的Driver注冊進驅動管理器中
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
所以,整個程序是:
- Class.forName會使用應用類加載器加載Driver實作類
- 加載Driver實作類需要執行靜態方法,即將mysql的Driver注冊進驅動管理器中,那么此時需要加載DriverManager類
- 應用類加載器去加載DriverManager類,而DriverManager位于rt.jar中,便一直向上委托到啟動類加載器完成加載
這個程序確實沒有破壞雙親委派
那么jdbc4.0后的情況呢?
為了使用該特性,我們需要引入高版本的mysql-connector-java,這里引入的版本是5.1.8
此時完全可以拋棄第一行的Class.forName陳述句了,使用以下陳述句來進行實驗
Enumeration<Driver> en = DriverManager.getDrivers();
while (en.hasMoreElements()) {
java.sql.Driver driver = en.nextElement();
System.out.println(driver);
}
輸出為:

看來記憶體中已經存在mysql的Driver了,這到底是怎么做的呢?
應用類加載器逐層委托到啟動類加載器去加載DriverManager時,會同時執行它的靜態方法
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
loadInitialDrivers內部核心的代碼這有這兩句
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
看到ServiceLoader,大家想到了什么,這不是jdk spi機制的核心嗎?
spi機制在我的這篇文章SpringBoot的自動裝配原理、自定義starter與spi機制,一網打盡有詳細的一個介紹,并且對比了SpringBoot與JDK中spi機制的異同,
既然使用到了spi機制,那么mysql-connector-java的jar包在META-INF目錄下必然有services目錄,內容如下,

啟動類加載DriverManager,之后需要通過spi機制去加載jar包中的Driver類,而該Driver理應被應用類加載器加載,這個時候就需要啟動類加載器去通知應用類加載器,這明顯違背了雙親委派機制,
那么,啟動類加載器是怎么去通知應用類加載器的呢?
我們繼續進入到ServiceLoader.load方法中
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Thread.currentThread().getContextClassLoader()是執行緒背景關系類加載器,看來最終使用的是執行緒背景關系類加載器去加載的Driver實作類,
而在sun.misc.Launcher類中,將應用類加載器設定進了執行緒背景關系類加載器中,所以可以理解為,通過執行緒背景關系類加載器,我們可以拿到應用類加載器的參考,
public Launcher() {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
}
在jdbc4.0的情況下,梳理一下整個程序:
- 應用類加載器逐層委托到啟動類加載器去加載DriverManager類
- 啟動類加載器加載DriverManager類時,會執行其靜態方法,即通過spi機制去加載jar包中的Driver實作類
- 此時啟動類加載器需要委托應用類加載器加載Driver實作類,具體做法是通過執行緒背景關系類加載器拿到應用類加載器的參考
確實是破壞了雙親委派!
七、Tomcat對雙親委派的破壞
tomcat有兩個最基礎的知識點,一個是應用打包放在webapps目錄下就可以運行,另外一個是修改jsp會實時生效,
那這里拋出幾個問題,來猜想一下Tomcat中類加載器的一個結構,
(1)jsp實時生效是怎么做的?
首先,在jvm中,如何去確定類的唯一性呢?是由類加載器實體+全限定名一起確定的,全限定名相同,類加載器不同,則會被認定為不同的類,
jsp檔案被修改后,會被重新編譯成Servlet,全限定名肯定是不變的,如果這個時候不去卸載加載該Servlet的類加載器,那么新jsp是無論如何都不會被加載進來的,因此,我們可以得知,每一個jsp檔案都會對應一個類加載器實體,
(2)每個webapps下的應用依賴的類別庫是否會互相影響?
顯然是不會影響的,應用A依賴低版本的Spring,而應用B依賴高版本的Spring,都是允許的,雖然Spring的版本不同,但某些類的全限定名是完全一致的,如果應用A與應用B采用同一個類加載器,是不會允許Spring版本不一樣的,這里,我們猜想webapps下的每一個應用都會對應一個不同的類加載器實體,用以保持應用間的隔離,
從以上的兩個問題,我們可以了解到:每一個jsp(或者說servlet)都對應一個不同的類加載器實體,每個webapp應用也是,
其實,tomcat5版本(以下如果沒有另外宣告版本,那么都是以該版本為例)的類加載器結構為:

其中各個加載器加載的范圍為:
- Common ClassLoader:主要加載common目錄下的資源
- Catalina ClassLoader:主要加載server目錄下的資源
- Shared ClassLoader:主要加載shared目錄下的資源
- Webapp ClassLoader:每一個應用會對應與該型別的一個實體,主要加載該應用下的WEB-INF下的資源
- JasperLoader:每一個jsp檔案會對應于該型別的一個實體,就是為了修改jsp能及時生效
(前三個類加載器在tomcat6中已經合并了,合并之后的加載器加載lib目錄下的資源)
它們在Bootstrap類中有過宣告:
protected ClassLoader commonLoader = null;
protected ClassLoader catalinaLoader = null;
protected ClassLoader sharedLoader = null;
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
其中createClassLoader方法會從container\catalina\src\conf\catalina.properties配置中讀取每個加載器加載的范圍:
common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar
server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar
shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar
catalina.home是安裝目錄,catalina.base是每個tomcat實體的作業目錄,在只用一個tomcat的情況下,兩個目錄是一樣的,
在了解了加載器的型別與范圍之后,那么tomcat到底是怎么打破雙親委派機制的呢?
前面說過,雙親委派機制被定義在ClassLoader中的loadClass方法中,如果某個自定義的類加載想要打破雙親委派,那么重新loadClass方法即可,
Tomcat中的WebappClassLoader就是自定義類加載器,它的loadClass方法為:
public Class loadClass(String name) throws ClassNotFoundException {
return (loadClass(name, false));
}
public Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
//1、從自己的本地快取中查找,本地快取的資料結構為ResourceEntry
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//2、從jvm的快取中查找
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//3、如果快取中都找不到,則利用系統類加載器加載
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
//4、開啟代理的話,則使用父加載器加載
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
//5、自行加載
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
//如果自己也加載不了,那就只能讓父加載器加載了
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
throw new ClassNotFoundException(name);
}
loadClass內部的邏輯整理如下:
- 先從WebappClassLoader的ResourceEntry快取中查找
- 從jvm快取中查找,比如去元資料區查找
- 利用系統類(應用類)加載器加載,避免webapp中的類覆寫掉標準類別庫中的類,
- 開啟代理的話,則使用父加載器加載,這個默認沒開啟的,
- webappClassLoader自行去加載
- 自己也沒加載成功的話,最后只能讓父加載器去加載
這里有一個問題,對于一些非基礎類別庫,為什么要先讓webappClassLoader先去加載呢?
假設應用a依賴1.0版本的x.jar,而應用b依賴2.0版本的x.jar,為了保證兩個應用的隔離性,首先要做的就是保證兩個應用各自對應不同的webappClassLoader實體,如果這兩個webappClassLoader實體在加載x.jar的時候,直接向上委托,那么最終只會加載一個版本的x.jar,
從上面,我們可以了解到:
對于一些標準類別庫中的類,比如Object類,會讓系統類加載器加載,然后一直委托到啟動類加載器,這個程序是沒有違背雙親委派的,
而對于webapp中獨有的類,則是webappClassLoader自行去加載,加載失敗才讓父加載器加載,明顯是違背雙親委派的,
八、總結
雙親委派機制,核心是子加載器委托父加載器,能夠避免java核心類別庫被篡改,增加了安全性,
但發展會帶來創新,創新就會帶來變革,jdbc與tomcat打破了這個自古相傳的機制,
在jdbc中,父加載器委托子加載器,即利用執行緒背景關系類加載器,讓啟動類加載器得以委托應用類加載器,去加載jar中的資料庫驅動,
在tomcat中,子加載器優先于父加載器加載,即為了實作各個webapp的隔離性,webappClassLoader會先于父加載器加載,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/298103.html
標籤:java
上一篇:老寇云-java技術堆疊進階-武俠篇-位運算之異或運算
下一篇:java基礎-陣列(回顧)
