淺顯易懂的帶你掌握雙親委派模型

三種類加載器

啓動類加載器(Bootstrap ClassLoader)

負責加載<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數所指定的路徑存放的,可以被虛擬機所識別的類庫加載到虛擬機的內存中,這個類加載器的底層是由C++實現的,是虛擬機當中的一部分,其它類加載器都是由Java實現的,獨立於虛擬機之外,所有繼承自java.lang.ClassLoader抽象類。java

在啓動類加載器執行時,會加載一個很重要的類:sun.misc.Launcher,這個類裏面含有兩個靜態內部類:app

  1. ExtClassLoader擴展類加載器
  2. AppClassLoader應用程序加載器

在Launcher類加載完成之後會對該類進行初始化,在初始化過程當中會建立兩個類加載器的實例,源碼以下所示:ide

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
}
複製代碼

擴展類加載器(Extension ClassLoader)

負責加載<JAVA_HOME>\lib\ext目錄下,或者被java.ext.dirs系統變量所指定的路徑中全部的類庫。源碼分析

應用程序類加載器(Application ClassLoader)

也被稱爲「系統類加載器」,負責加載用戶類路徑(java -classpath)上的全部類庫。若是沒有自定義類加載器,那麼這個加載器就是默認的類加載器。能夠經過ClassLoadergetSystemClassLoader()獲取該類加載器的實例。post

注意:每種類加載器加載的類信息都會存放在方法區的不一樣區域上,因此不一樣的類加載器若是加載相同的一個類字節碼文件,在虛擬機看來生成的兩個類對象是不相同的!若是有兩個相同的類User,經過Application ClassLoader加載和Extension ClassLoader加載出來的兩個User類對象,是不相同的。測試

雙親委派模型

雙親委派的工做過程

一個類加載器收到類加載的請求時,它不會立刻加載該類,而是把這個請求委託給父加載器去完成,每個層次的類加載器都是如此,所以全部的類加載請求都必須先經過啓動類加載器嘗試加載,只有當父加載器沒法加載這個類時,纔會把加載請求傳遞給它的子加載器去嘗試加載,流程以下:this

雙親委派模型的做用

使用雙親委派模型來組織類加載器之間的關係,一個顯而易見的好處就是Java中的類隨着它的加載器一塊兒具有了一種帶有優先級的層次關係。例如java.lang.Object存放在rt.jar當中,不管哪一層的類加載器須要加載Object類,最終都是委派到啓動類加載器進行加載,因此能夠保證Object類在程序的各類類加載器環境中是同一個類spa

不一樣的類加載器加載同一個類會致使出現相同的字節碼文件產生不一樣的Class實例信息,即每一個加載器加載同一個字節碼文件會保存在方法區的不一樣位置,因此若是不用雙親委派模型,若是用戶本身寫了一個java.lang.Object類,並放在程序的classpath當中,那麼系統中會出現多個版本的Object類,那麼程序就會一片混亂,使用Object類時不知道該哪一個加載器加載的Class實例。.net

以下圖,在方法區中每一個加載器都有本身的一個區域存儲本身加載的類類型信息,因此雙親委派模型的重要性體如今此。3d

雙親委派模型的源碼實現

protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先檢查類是否已經被加載過了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //交給父加載器去嘗試加載該類
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //若是父加載器拋出ClassNotFoundException異常,說明父加載器沒法加載這個類
            }

            if (c == null) {
                //若是沒有加載這個類,按順序調用findClass方法去嘗試加載這個類
                long t1 = System.nanoTime();
                c = findClass(name);

                //記錄這個類的裝入信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
複製代碼

雙親委派模型的特色

  1. 父加載器的方法區內存儲的類信息對子加載器是可見的,由於子加載器收到類加載請求後會先委派給父加載器,若是父加載器中已經加載了這個類,會直接返回這個類的信息給子加載器,就不用繼續加載了。
  2. 雙親委派模型下每一個類都是惟一的,只會由一個類加載器加載。

違背雙親委派的自定義類加載器

咱們能夠繼承ClassLoader類,覆蓋loadClass()而且在代碼中不委派給父加載器,而且覆蓋findClass()方法定義本身的類加載規則。

咱們定義與系統類庫相同的一個類sun.applet.Main,並用自定義的類加載器進行類加載,將其與JVM默認加載的sun.applet.Main做類型判斷,判斷兩個類是否相同。

測試Demo的結構圖以下,按照結構圖建立項目,把代碼複製進去就必定沒有錯,曾經做者在找類路徑這一塊卡了一個晚上······

第y一步,自定義sun.applet.Main

package sun.applet;

/** * @author Zeng * @date 2020/4/10 8:22 */
public class Main {
    static {
        System.out.println("customized sun.applet.Main constructed");
    }
    public static void main(String[] args) {
        System.out.println("recognized as sun.applet.Main in jdk," +
                " and there isn't any main method");
    }
}

複製代碼

第二步,自定義類加載器UnDelegationClassLoader

package sun.applet;

import java.io.*;

/** * @author Zeng * @date 2020/4/10 8:01 */
public class UnDelegationClassLoader extends ClassLoader {

    private String classpath;

    public UnDelegationClassLoader(String classpath) {
        super(null);
        this.classpath = classpath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        InputStream is = null;
        try {
            String classFilePath = this.classpath + name.replace(".", "/") + ".class";
            is = new FileInputStream(classFilePath);
            byte[] buf = new byte[is.available()];
            is.read(buf);
            return defineClass(name, buf, 0, buf.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    throw new IOError(e);
                }
            }
        }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> clz = findLoadedClass(name);
        if (clz != null) {
            return clz;
        }
        // jdk 目前對"java."開頭的包增長了權限保護,這些包咱們仍然交給 jdk 加載
        if (name.startsWith("java.")) {
            return ClassLoader.getSystemClassLoader().loadClass(name);
        }
        return findClass(name);
    }
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
        sun.applet.Main obj1 = new sun.applet.Main();
        UnDelegationClassLoader unDelegationClassLoader = new UnDelegationClassLoader("./out/production/classloading/");
        System.out.println(unDelegationClassLoader.findClassPath());
        String name = "sun.applet.Main";
        Class<?> clz = unDelegationClassLoader.loadClass(name);
        Object obj2 = clz.newInstance();
        System.out.println("obj1 class: "+obj1.getClass());
        System.out.println("obj2 class: "+obj2.getClass());
        System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
        System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
        System.out.println("obj1 == obj2" + obj1 == obj2);
    }
}
複製代碼

輸出結果以下

能夠看到Java類庫中Main的實例obj1和自定義的obj2實例的類對象都是sun.applet.Main,可是因爲類加載器不一樣,因此obj2與Java類庫中的Main對象是不相同的。因此若是不進行雙親委派,若是第三方依賴須要調用sun.applet.Main,就會變得糊塗,不知道該調用哪個Main

自定義類加載器的正確姿式

自定義類加載器DelegationClassLoader,並委託給父加載器AppClassLoader

package sun.applet;

import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;

/** * @author Zeng * @date 2020/4/10 7:23 */
public class DelgationClassLoader extends ClassLoader {

    private String classpath;

    public DelgationClassLoader(String classpath, ClassLoader parent) {
        super(parent);
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        InputStream is = null;
        try {
            String fileClasspath = this.classpath + name.replace(".", "\\") + ".class";
            is = new FileInputStream(fileClasspath);
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        }catch (Exception e){
            throw new ClassNotFoundException(name);
        }finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    throw new IOError(e);
                }
            }
        }

    }
}

複製代碼

第二步,測試加載重名類sun.applet.Main,因爲篇幅問題,只貼出核心方法

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
        sun.applet.Main obj1 = new sun.applet.Main();
        DelgationClassLoader unDelegationClassLoader = new DelgationClassLoader("./out/production/classloading/", ClassLoader.getSystemClassLoader());
        String name = "sun.applet.Main";
        Class<?> clz = unDelegationClassLoader.loadClass(name);
        Object obj2 = clz.newInstance();
        System.out.println("obj1 class: "+obj1.getClass());
        System.out.println("obj2 class: "+obj2.getClass());
        System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
        System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
        System.out.println("obj1 instanceof sun.applet.Main: " + (obj1 instanceof sun.applet.Main));
        System.out.println("obj2 instanceof sun.applet.Main: " + (obj2 instanceof sun.applet.Main));
    }
複製代碼

輸出結果以下圖所示:

能夠看到不管是Java類庫中的sun.applet.Main仍是自定義的sun.applet.Main,JVM都交給了啓動類加載器去加載它們,因此在第三方依賴但願調用sun.applet.Main時,它會很是清楚,只能調用這一個Class對象。因此這也解釋了爲何雙親委派模型能夠肯定系統中只有一個惟一的類。

總結:

這篇文章主要講解了三種系統默認擁有的類加載器什麼是雙親委派模型、雙親委派模型的工做流程、源碼分析以及它的特色,使用實際案例演示若是違背雙親委派模型會帶來什麼後果,反映出雙親委派模型的好處。固然,雙親委派模型不是一種強制約束,它只是虛擬機的其中一種實現,如Tomcat、JDBC等技術就破壞了雙親委派模型,有興趣的讀者能夠去深刻了解,做者因爲能力有限、時間有限,沒法講解更多更詳細深刻的知識點。若是這篇文章對你有小小的幫助,你的點贊是對我最大的鼓勵和支持!感謝你的閱讀!

巨人的肩膀:

《深刻理解Java虛擬機》第三版

juejin.im/post/5e479c…

blog.csdn.net/lengxiao199…

相關文章
相關標籤/搜索