Java雙親委派模型:爲什麼要雙親委派?如何打破它?破在哪裏?---todo

文章目錄
一、前言
二、類加載器
三、雙親委派機制

  • 1、什麼是雙親委派
  • 2、爲什麼要雙親委派?

四、破壞雙親委派

  • 1、直接自定義類加載器加載
  • 2、跳過AppClassLoader和ExtClassLoader
  • 3、自定義類加載器加載擴展類
  • 4、Tomcat中破壞雙親委派的場景
  • 5、一個比較完整的自定義類加載器

五、Class.forName和ClassLoader.loadClass區別
六、線程上下文類加載器
七、要點回顧

 

一、前言

平時做業務開發比較少接觸類加載器,但是如果想深入學習Tomcat、Spring等開源項目,或者從事底層架構的開發,瞭解甚至熟悉類加載的原理是必不可少的。

java的類加載器有哪些?什麼是雙親委派?爲什麼要雙親委派?如何打破它?多多少少對這些概念瞭解一些,甚至因爲應付面試背過這些知識點,但是再深入一些細節,卻知之甚少。

 

二、類加載器


類加載器,顧名思義就是一個可以將Java字節碼加載爲java.lang.Class實例的工具。這個過程包括,讀取字節數組、驗證、解析、初始化等。另外,它也可以加載資源,包括圖像文件和配置文件。

類加載器的特點:

  • 動態加載,無需在程序一開始運行的時候加載,而是在程序運行的過程中,動態按需加載,字節碼的來源也很多,壓縮包jar、war中,網絡中,本地文件等。類加載器動態加載的特點爲熱部署,熱加載做了有力支持。
  • 全盤負責,當一個類加載器加載一個類時,這個類所依賴的、引用的其他所有類都由這個類加載器加載,除非在程序中顯式地指定另外一個類加載器加載。所以破壞雙親委派不能破壞擴展類加載器以上的順序。

一個類的唯一性由加載它的類加載器和這個類的本身決定(類的全限定名+類加載器的實例ID作爲唯一標識)。比較兩個類是否相等(包括Class對象的equals()、isAssignableFrom()、isInstance()以及instanceof關鍵字等),只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,這兩個類就必定不相等。

從實現方式上,類加載器可以分爲兩種:一種是啓動類加載器,由C++語言實現,是虛擬機自身的一部分;另一種是繼承於java.lang.ClassLoader的類加載器,包括擴展類加載器、應用程序類加載器以及自定義類加載器。

啓動類加載器(Bootstrap ClassLoader):負責加載<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果想設置Bootstrap ClassLoader爲其parent,可直接設置null。

擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定路徑中的所有類庫。該類加載器由sun.misc.Launcher$ExtClassLoader實現。擴展類加載器由啓動類加載器加載,其父類加載器爲啓動類加載器,即parent=null。

應用程序類加載器(Application ClassLoader):負責加載用戶類路徑(ClassPath)上所指定的類庫,由sun.misc.Launcher$App-ClassLoader實現。開發者可直接通過java.lang.ClassLoader中的getSystemClassLoader()方法獲取應用程序類加載器,所以也可稱它爲系統類加載器。應用程序類加載器也是啓動類加載器加載的,但是它的父類加載器是擴展類加載器。在一個應用程序中,系統類加載器一般是默認類加載器。

 

 

 

三、雙親委派機制

1、什麼是雙親委派

JVM 並不是在啓動時就把所有的.class文件都加載一遍,而是程序在運行過程中用到了這個類纔去加載。除了啓動類加載器外,其他所有類加載器都需要繼承抽象類ClassLoader,這個抽象類中定義了三個關鍵方法,理解清楚它們的作用和關係非常重要。

public abstract class ClassLoader {

    //每個類加載器都有個父加載器
    private final ClassLoader parent;
    
    public Class<?> loadClass(String name) {
  
        //查找一下這個類是不是已經加載過了
        Class<?> c = findLoadedClass(name);
        
        //如果沒有加載過
        if( c == null ){
          //先委派給父加載器去加載,注意這是個遞歸調用
          if (parent != null) {
              c = parent.loadClass(name);
          }else {
              // 如果父加載器爲空,查找Bootstrap加載器是不是加載過了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加載器沒加載成功,調用自己的findClass去加載
        if (c == null) {
            c = findClass(name);
        }
        
        return c;
    }
    
    protected Class<?> findClass(String name){
       //1. 根據傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內存
          ...
          
       //2. 調用defineClass將字節數組轉成Class對象
       return defineClass(buf, off, len);
    }
    
    // 將字節碼數組解析成一個Class對象,用native方法實現
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }
}

 

 

從上面的代碼可以得到幾個關鍵信息:

  • JVM 的類加載器是分層次的,它們有父子關係,而這個關係不是繼承維護,而是組合,每個類加載器都持有一個 parent 字段,指向父加載器。(AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent=null。)
  • defineClass 方法的職責是調用 native 方法把 Java 類的字節碼解析成一個 Class 對象。
  • findClass 方法的主要職責就是找到.class文件並把.class文件讀到內存得到字節碼數組,然後調用 defineClass 方法得到 Class 對象。子類必須實現findClass 。
  • loadClass 方法的主要職責就是實現雙親委派機制:首先檢查這個類是不是已經被加載過了,如果加載過了直接返回,否則委派給父加載器加載,這是一個遞歸調用,一層一層向上委派,最頂層的類加載器(啓動類加載器)無法加載該類時,再一層一層向下委派給子類加載器加載。

 

2、爲什麼要雙親委派?


雙親委派保證類加載器,自下而上的委派,又自上而下的加載,保證每一個類在各個類加載器中都是同一個類。

一個非常明顯的目的就是保證java官方的類庫<JAVA_HOME>\lib和擴展類庫<JAVA_HOME>\lib\ext的加載安全性,不會被開發者覆蓋。

例如類java.lang.Object,它存放在rt.jar之中,無論哪個類加載器要加載這個類,最終都是委派給啓動類加載器加載,因此Object類在程序的各種類加載器環境中都是同一個類。

如果開發者自己開發開源框架,也可以自定義類加載器,利用雙親委派模型,保護自己框架需要加載的類不被應用程序覆蓋。

 

四、破壞雙親委派


雙親委派模型並不是一個具有強制性約束的模型,而是Java設計者推薦給開發者們的類加載器實現方式。這個委派和加載順序完全是可以被破壞的。

如果想自定義類加載器,就需要繼承ClassLoader,並重寫findClass,如果想不遵循雙親委派的類加載順序,還需要重寫loadClass。

1、直接自定義類加載器加載


如下是一個自定義的類加載器TestClassLoader,並重寫了findClass和loadClass:

 

public class TestClassLoader extends ClassLoader {
    public TestClassLoader(ClassLoader parent) {
        super(parent);
    }
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        // 1、獲取class文件二進制字節數組
        byte[] data = null;
        try {
            System.out.println(name);
            String namePath = name.replaceAll("\\.", "\\\\");
            String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            FileInputStream fis = new FileInputStream(new File(classFile));
            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、字節碼加載到 JVM 的方法區,
        // 並在 JVM 的堆區建立一個java.lang.Class對象的實例
        // 用來封裝 Java 類相關的數據和方法
        return this.defineClass(name, data, 0, data.length);
    }
    @Override
    public Class loadClass(String name) throws ClassNotFoundException{
        Class clazz = null;
        // 直接自己加載
        clazz = this.findClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 自己加載不了,再調用父類loadClass,保持雙親委託模式
        return super.loadClass(name);
    }
}

 

測試:
初始化自定義的類加載器,需要傳入一個parent,指定其父類加載器,那就先指定爲加載TestClassLoader的類加載器爲TestClassLoader的父類加載器吧:

public static void main(String[] args) throws Exception {
        // 初始化TestClassLoader,被將加載TestClassLoader類的類加載器設置爲TestClassLoader的parent
        TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());
        System.out.println("TestClassLoader的父類加載器:" + testClassLoader.getParent());
        // 加載 Demo
        Class clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");
        System.out.println("Demo的類加載器:" + clazz.getClassLoader());
    }

 


運行如下測試代碼,發現報錯了:
找不到java\lang\Object.class,我加載study.stefan.classLoader.Demo類和Object有什麼關係呢?

 

 

轉瞬想到java中所有的類都隱含繼承了超類Object,加載study.stefan.classLoader.Demo,也會加載父類Object。Object和study.stefan.classLoader.Demo並不在同個目錄,那就找到Object.class的目錄(將jre/lib/rt.jar解壓),修改TestClassLoader#findClass如下:
遇到前綴爲java.的就去找官方的class文件。

 

 

運行測試代碼:
還是報錯了!!!

 

 

報錯信息爲:Prohibited package name: java.lang。
跟了下異常堆棧:
TestClassLoader#findClass最後一行代碼調用了java.lang.ClassLoader#defineClass,
java.lang.ClassLoader#defineClass最終調用瞭如下代碼:

 

 

 

 


看意思是java禁止用戶用自定義的類加載器加載java.開頭的官方類,也就是說只有啓動類加載器BootstrapClassLoader才能加載java.開頭的官方類。

得出結論,因爲java中所有類都繼承了Object,而加載自定義類study.stefan.classLoader.Demo,之後還會加載其父類,而最頂級的父類Object是java官方的類,只能由BootstrapClassLoader加載。

 

2、跳過AppClassLoader和ExtClassLoader


既然如此,先將study.stefan.classLoader.Demo交由BootstrapClassLoader加載即可。
由於java中無法直接引用BootstrapClassLoader,所以在初始化TestClassLoader時,傳入parent爲null,也就是TestClassLoader的父類加載器設置爲BootstrapClassLoader:

package com.stefan.DailyTest.classLoader;

public class Test {
    public static void main(String[] args) throws Exception {
        // 初始化TestClassLoader,並將加載TestClassLoader類的類加載器
        // 設置爲TestClassLoader的parent
        TestClassLoader testClassLoader = new TestClassLoader(null);
        System.out.println("TestClassLoader的父類加載器:" + testClassLoader.getParent());
        // 加載 Demo
        Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");
        System.out.println("Demo的類加載器:" + clazz.getClassLoader());
    }
}

 

雙親委派的邏輯在 loadClass,由於現在的類加載器的關係爲TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中無需重寫loadClass。

運行測試代碼:

 

 

成功了,Demo類由自定義的類加載器TestClassLoader加載的,雙親委派模型被破壞了。

如果不破壞雙親委派,那麼Demo類處於classpath下,就應該是AppClassLoader加載的,所以真正破壞的是AppClassLoader這一層的雙親委派。

 

3、自定義類加載器加載擴展類


假設classpath下由上述TestClassLoader加載的類中用到了<JAVA_HOME>\lib\ext下的擴展類,那麼這些擴展類也會由TestClassLoader加載,但是會報類文件找不到的情況。
但是自定義類加載器也是能加載<JAVA_HOME>\lib\ext下的擴展類的,只要自定義類加載器能找準擴展類的類路徑。

以擴展目錄com.sun.crypto.provider下的類舉例:
(1)Demo中隨便引用一個擴展類:

import com.sun.crypto.provider.ARCFOURCipher;
public class Demo {
public Demo() {
ARCFOURCipher arcfourCipher = new ARCFOURCipher();
System.out.println("ARCFOURCipher.getClassLoader=" + arcfourCipher.getClass().getClassLoader());
}
}
1
2
3
4
5
6
7
(2)修改TestClassLoader#findClass:

(3)測試代碼中需要調用一下Demo類的構造器:

(4)運行測試代碼
自定義類加載器成功加載了擴展類。

由上得出結論,<JAVA_HOME>\lib\ext下的擴展類是沒有強制只有ExtClassLoader能加載,自定義類加載器也能加載。

 

4、Tomcat中破壞雙親委派的場景


只有官方庫java.的類必須由啓動類加載器加載,無法破壞,擴展類加載器和應用程序類加載器的雙親委派都是可以破壞的。

知道了理論,還需要根據實際場景,找準破壞雙親委派的位置。可以看看優秀的開源框架中是如何破壞雙親委派的,比如Tomcat:


Tomcat源碼就不貼了,Tomcat中可以部署多個web項目,爲了保證每個web項目互相獨立,所以不能都由AppClassLoader加載,所以自定義了類加載器WebappClassLoader,WebappClassLoader繼承自URLClassLoader,重寫了findClass和loadClass,並且WebappClassLoader的父類加載器設置爲AppClassLoader。
WebappClassLoader.loadClass中會先在緩存中查看類是否加載過,沒有加載,就交給ExtClassLoader,ExtClassLoader再交給BootstrapClassLoader加載;都加載不了,才自己加載;自己也加載不了,就遵循原始的雙親委派,交由AppClassLoader遞歸加載。

 

5、一個比較完整的自定義類加載器


一般情況下,自定義類加載器都是繼承URLClassLoader,具有如下類關係圖:


public class TestClassLoader extends URLClassLoader {
public TestClassLoader(ClassLoader parent) {
super(new URL[0], parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1、先自己的路徑找
Class<?> clazz = null;
try {
clazz = findClassInternal(name);
} catch (Exception e) {
// Ignore
}
if (clazz != null) {
return clazz;
}
// 在 父類路徑 找
return super.findClass(name);
}

private Class<?> findClassInternal(String name) throws IOException {
byte[] data = null;
try {
String dir = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\";
String namePath = name.replaceAll("\\.", "\\\\");
String classFile = dir + namePath + ".class";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(new File(classFile));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
data = baos.toByteArray();
// 字節碼加載到 JVM 的方法區,
// 並在 JVM 的堆區建立一個java.lang.Class對象的實例
// 用來封裝 Java 類相關的數據和方法
return this.defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw e;
}
}
@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、自己加載不了,再調用父類loadClass,保持雙親委託模式
return super.loadClass(name);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

五、Class.forName和ClassLoader.loadClass區別


forName(String name, boolean initialize,ClassLoader loader) 可以指定classLoader。
不顯式傳classLoader就是默認當前類的類加載器:
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
1
2
3
4
5
類加載過程:加載——》驗證——》準備——》解析——》類初始化——》使用(對象實例初始化)——》卸載

java.lang.Class.forName 會調用到forName0方法,第二個參數 initialize = true,意爲會進行類初始化(<clinit>())操作。


java.lang.ClassLoader.loadClass 會調用到 protected 修飾的 loadClass(String name, boolean resolve),第2個參數resolve=false,意爲不進行類的解析操作,也就不會進行類初始化,包括靜態變量的初始化、靜態代碼塊的運行,都不會進行。

 


六、線程上下文類加載器


線程上下文類加載器其實是一種類加載器傳遞機制。可以通過java.lang.Thread#setContextClassLoader方法給一個線程設置上下文類加載器,在該線程後續執行過程中就能把這個類加載器取(java.lang.Thread#getContextClassLoader)出來使用。

如果創建線程時未設置上下文類加載器,將會從父線程(parent = currentThread())中獲取,如果在應用程序的全局範圍內都沒有設置過,就默認是應用程序類加載器。

線程上下文類加載器的出現就是爲了方便破壞雙親委派:

一個典型的例子便是JNDI服務,JNDI現在已經是Java的標準服務,它的代碼由啓動類加載器去加載(在JDK 1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代碼,但啓動類加載器不可能去加載ClassPath下的類。

但是有了線程上下文類加載器就好辦了,JNDI服務使用線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。

Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

摘自《深入理解java虛擬機》周志明

 

七、要點回顧


java 的類加載,就是獲取.class文件的二進制字節碼數組並加載到 JVM 的方法區,並在 JVM 的堆區建立一個用來封裝 java 類相關的數據和方法的java.lang.Class對象實例。
java默認有的類加載器有三個,啓動類加載器(BootstrapClassLoader),擴展類加載器(ExtClassLoader),應用程序類加載器(也叫系統類加載器)(AppClassLoader)。類加載器之間存在父子關係,這種關係不是繼承關係,是組合關係。如果parent=null,則它的父級就是啓動類加載器。啓動類加載器無法被java程序直接引用。
雙親委派就是類加載器之間的層級關係,加載類的過程是一個遞歸調用的過程,首先一層一層向上委託父類加載器加載,直到到達最頂層啓動類加載器,啓動類加載器無法加載時,再一層一層向下委託給子類加載器加載。
加載一個類時,也會加載其父類,如果該類中還引用了其他類,則按需加載,且類加載器都是加載當前類的類加載器。
雙親委派的目的主要是爲了保證java官方的類庫<JAVA_HOME>\lib加載安全性,不會被開發者覆蓋。
<JAVA_HOME>\lib 和<JAVA_HOME>\lib\ext是java官方核心類庫,一般不會去破壞ExtClassLoader及其以上的雙親委派。
破壞雙親委派有兩種方式:第一種,自定義類加載器,必須重寫findClass和loadClass;第二種是通過線程上下文類加載器的傳遞性,讓父類加載器中調用子類加載器的加載動作。
ClassLoader.loadClass 和 Class.forName 區別在於,ClassLoader.loadClass 不會對類進行解析和類初始化,而 Class.forName 是有正常的類加載過程的。
參考:

《深入理解java虛擬機》周志明(書中對類加載的介紹非常詳盡,部分精簡整理後引用。)
《深入拆解Tomcat & Jetty》Tomcat如何打破雙親委託機制?李號雙
《Tomcat內核設計剖析》汪建,第十三章 公共與隔離的類加載器

原文鏈接:https://blog.csdn.net/weixin_36586120/article/details/117457014

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章