java類加載流程之雙親委派與破壞

雙親委派是什么?

1、首先我們看下如何自定義一個類加載器
自定義類加載器需要繼承ClassLoader類,并重寫loadClass(String name, boolean resolve)、loadClass(String name)和findClass(String name)方法;
自定義類加載器中被重寫的loadClass方法將由項目代碼調(diào)用。

public class MyClassLoader extends ClassLoader{

    //用于指定類加載器要加載的類地址
    private String classPath;

    //無參構(gòu)造方法
    public MyClassLoader(String classPath){
        super();
        this.classPath = classPath;
    }

    //帶參構(gòu)造方法,
    //參數(shù)是ClassLoader parent,用于指定當前類加載器的父加載器
    public MyClassLoader(String classPath, ClassLoader parent){
        super(parent);
        this.classPath = classPath;
    }

    /**
     * 最終會由這個方法來調(diào)用findClass方法
     * 這個方法中體現(xiàn)了類的雙親委派加載
     * 想要打破雙親委派也是在這個方法實現(xiàn)
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        //判斷類是否已經(jīng)加載過了,如果加載過了則直接返回即可
        Class<?> loadedClass = findLoadedClass(name);

        ClassLoader systemClassLoader = getSystemClassLoader();
        loadedClass = super.loadClass(name, resolve);

        //如果需要即時解析,則進行解析操作
        if (resolve) resolveClass(loadedClass);

        //返回加載的Class對象
        return loadedClass;

    }

    /**
     * 這個方法與上面那個方法的作用是一樣的,唯一的區(qū)別是,這個方法沒有resolve參數(shù)
     * 默認加載后先不解析
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return this.loadClass(name, false);
    }


    /**
     * 其作用是從磁盤讀取字節(jié)碼文件到內(nèi)存
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(classPath);
        try {
            /**
             * 讀字節(jié)流,不需要reader
             */
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = in.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }

            byte[] data = baos.toByteArray();

            if (data == null){
                throw new ClassNotFoundException("找不到目標類,加載目標類失敗...");
            } else {
                /**
                 * 將讀取的字節(jié)碼數(shù)據(jù)交給jvm去構(gòu)建成字節(jié)碼對象
                 */
                return defineClass(name, data, 0, data.length);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

}

測試類:

public class TestClassLoad {

    public static void main(String[] args) {

        TestClassLoad testClassLoad = new TestClassLoad();

        //指定當前加載器的父加載器為AppClassLoader
        MyClassLoader myClassLoader = new MyClassLoader("F:\\cc\\Person.class", ClassLoader.getSystemClassLoader());
        System.out.println("myClassLoader的父加載器是: " + myClassLoader.getParent());

        try {
            //加載的目標類是test.Person,在外界代碼中調(diào)用loadClass方法
            Class<?> loadClass = myClassLoader.loadClass("test.Person");
            //打印加載的類是不是目標類
            System.out.println(loadClass.getName());
            //獲取當前已加載的Class對象的類加載器是什么
            System.out.println(loadClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}

再分析下ClassLoader類的構(gòu)造方法:

它有3個構(gòu)造方法,兩個protected的和一個private的;
protected ClassLoader(),無參構(gòu)造方法 ;
protected ClassLoader(ClassLoader parent),帶參構(gòu)造方法,可以用于指定當前類加載器的父加載器;
private ClassLoader(Void unused, ClassLoader parent) ,類內(nèi)部使用的構(gòu)造方法, ClassLoader()和ClassLoader(ClassLoader parent)都是調(diào)用的這個構(gòu)造方法做初始化。

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

    /**
     * Creates a new class loader using the specified parent class loader for
     * delegation.
     *
     * 創(chuàng)建自定義類加載器對象時,指定parent類加載器
     *
     * @since  1.2
     */
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

    /**
     * Creates a new class loader using the <tt>ClassLoader</tt> returned by
     * the method {@link #getSystemClassLoader()
     * <tt>getSystemClassLoader()</tt>} as the parent class loader.
     *
     * 不帶參數(shù),默認通過getSystemClassLoader()方法創(chuàng)建類加載器的父加載器
     *
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

ClassLoader()無參構(gòu)造方法因為不帶參數(shù),所以沒法通過外界指定當前類加載器的父加載器,在它的默認實現(xiàn)中,它默認通過getSystemClassLoader()方法來指定當前類加載器的父加載器;
可以看到這個方法的返回值是ClassLoader類, 這個方法的作用是創(chuàng)建系統(tǒng)類加載類對象scl,并返回scl對象作為自定義類加載器對象的父加載器

下面重點分析下initSystemClassLoader()方法:

private static synchronized void initSystemClassLoader() {
    /**
     * 
     *  sclSet屬性是一個boolean類型的靜態(tài)屬性,它的作用是用于判斷system classloader是否已經(jīng)被創(chuàng)建了,
     * 如果已經(jīng)被創(chuàng)建了,則將sclSet屬性的值設置為true,否則它的值還是默認值false
     *
     * 
     * 能夠進入這個語句塊說明system classloader還沒被創(chuàng)建
     */
    if (!sclSet) {

        /**
         * 
         * scl表示system classloader,
         * 如果它不為null,卻還重新創(chuàng)建的話,就拋異常
         * 
         */
        if (scl != null)
            throw new IllegalStateException("recursive invocation");

        /**
         * 執(zhí)行到這里說明scl還沒被創(chuàng)建
         * 
         * getLauncher()獲取Launcher對象
         * 
         */
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

        /**
         * 
         * Launcher對象不為null
         * 
         */
        if (l != null) {
            Throwable oops = null;

            /**
             * 
             * 通過調(diào)用Launcher對象的getClassLoader()方法獲取Launcher類定義的ClassLoader對象
             */
            scl = l.getClassLoader();
            try {

                /**
                 * 
                 * 將上面獲取的類加載器封裝為系統(tǒng)類加載器
                 */
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }

        //系統(tǒng)類加載器創(chuàng)建完成之后,將sclSet標識設置為true
        sclSet = true;
    }
}

通過上面的分析我們可以知道,initSystemClassLoader()方法涉及到一個非常重要的類Launcher,下面我們要好好分析下Launcher類:

從上面的分析我們知道了initSystemClassLoader()方法會調(diào)用getLauncher()方法和getClassLoader()方法,我們來看下代碼:

getLauncher()方法的作用是返回Launcher類的Launcher launcher屬性,而launcher默認是已經(jīng)初始化的了,通過new Launcher()構(gòu)造方法創(chuàng)建了一個Launcher對象

我們再看下Launcher類的構(gòu)造方法邏輯:

因為initSystemClassLoader()方法還調(diào)用了getClassLoader()方法,我們再看下getClassLoader()方法的邏輯:
可以知道它是直接返回了Launcher類的loader對象引用,根據(jù)上面的分析我們知道,loader引用指向的是應用類加載器,所以調(diào)用Launcher類的getClassLoader()方法得到的是應用類加載器對象

至此,自定義類加載器的創(chuàng)建過程算是分析完成了。

此外,關于Launcher類還有一個需要分析的點:
Launcher類有三個靜態(tài)內(nèi)部類需要關注:
1)ExtClassLoader和AppClassLoader,它們都繼承自URLClassLoader
2)BootClassPathHolder

ExtClassLoader類

ExtClassLoader類加載器加載的類路徑由系統(tǒng)參數(shù)java.ext.dirs參數(shù)指定,默認是jdk/jre/lib/ext目錄下的jar包和.class文件

AppClassLoader類

AppClassLoader類加載器加載的類路徑由系統(tǒng)參數(shù)java.class.path指定,默認是jdk/jre/lib目錄下的某些jar包以及項目編譯目錄target\classes下的.class文件

還有一個是引導類加載器加載的類路徑是由系統(tǒng)參數(shù)sun.boot.class.path指定的,默認是jdk/jre/lib目錄下的某些jar包以及jdk/jre/classes目錄下的.class文件

根據(jù)上面的分析,我們知道自定義類加載器、應用類加載器、擴展類加載器和引導類加載器直接的關系如下圖所示:

2、上面已經(jīng)分析完了如何創(chuàng)建一個類加載器,如何指定類加載器的父加載器以及分析它的父加載器是什么:appClassLoader?null?看自己實例化了加載器時的選擇。

基于上述分析,我們已經(jīng)可以知道一個類加載器對象是怎么創(chuàng)建的了,接下來我們需要分析類加載器是如何加載類信息的:

重點分析ClassLoader類的loadClass方法邏輯:

ClassLoader類有l(wèi)oadClass(String name) 和 loadClass(String name, boolean resolve)方法,可以看到loadClass(String name)在ClassLoader類中實際上是調(diào)用了loadClass(String name, boolean resolve)方法的,它的resolve參數(shù)默認設置為false了

然后查看loadClass(String name, boolean resolve)方法的邏輯

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //加載的邏輯加鎖了,是線程安全的
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //這行代碼的邏輯是判斷指定類是否之前已經(jīng)加載過了,內(nèi)存中是否已經(jīng)存在了
            //如果已經(jīng)加載過了,則不需要重新加載,直接返回其Class對象即可
            Class<?> c = findLoadedClass(name);
            
            // 結(jié)果為null,說明之前還沒加載過,通過這個邏輯進行加載
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                        //這里表示如果當前類加載器的父加載器不為null,則調(diào)用父加載器的loadClass方法來加載目標類
                        //重點來了,java的雙親委派機制在這里就體現(xiàn)出來了:
                        //如果當前類加載器的父加載器不為null,就通過父加載器來加載器目標類,即將加載目標類的任務委托給了父加載器來執(zhí)行
                        //這里有遞歸調(diào)用的意味了:
                        //當前類加載器委托它的父加載器來加載目標類,它父加載器也會判斷下自己有沒有父加載器,如果有的話,也會委托給自己的父加載器來執(zhí)行加載任務,就這樣層層地委托下去,直到父加載器為null
                    if (parent != null) {
                        //如果父加載器為null,就將加載目標類的任務委托給引導類加載器來加載,也就是說通過雙親委派機制的層層委托之后,最終接收這個加載任務來執(zhí)行的是引導類加載器
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                //然后這里判斷下引導類加載器是否加載目標類成功了,如果成功則返回加載類信息得到的字節(jié)碼對象,
                //否則,通過本類加載器的findClass方法來加載目標類
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();

                    //通過本類加載器的findClass方法來加載目標類
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }

            //判斷是否需要加載后立即解析,如果需要的話則解析,否則先不解析
            if (resolve) {
                resolveClass(c);
            }

            //返回目標類的字節(jié)碼對象,可能為null
            return c;
        }
    }

通過上述分析,可以了解到,當前類加載器會將加載目標類的任務先委托給它的父加載器,然后父加載器也是同理先把加載目標類的任務委托給它的父加載器,...,就這樣層層委托,最后委托給引導類加載器,如果引導類加載器加載不到的話(因為它只能加載特定目錄下的,不在那個目錄下就加載不到),就由當前那個類加載器自己的加載邏輯findClass來加載,然后返回。

由于上述的委派給父類加載的流程其實是一個遞歸調(diào)用邏輯,所以就存在層層返回的情況,如果上一層返回后,沒有加載到目標類,那么就返回null;

當返回結(jié)果是null,那么就需要調(diào)用當前的類加載器的加載邏輯findClass來加載目標類,如果加載到了目標類,則直接層層返回即可。

4、上述過程解析了類加載器加載目標類的思路,分析我們常聽說的雙親委派機制到底是什么,如何實現(xiàn)委派的。

我們知道了雙親委派是什么樣的,那么我們應該如何破壞這個機制呢?

破壞雙親委派的正確思路:
因為一個最基本的java類最起碼都要繼承java.lang.Object類,而這個類是核心類,核心類是只能由引導類加載器進行加載,所以正確的實現(xiàn)邏輯應該是自己想要特殊加載的直接由自定義加載器加載,不向父加載器委托,但是呢實現(xiàn)邏輯里必須兼容的一點是對java核心類使用雙親委派機制,將核心類交由引導類加載器進行加載,以防直接使用自定義類加載器的話出現(xiàn)最基本的java.lang.Object類加載不到的情況

破壞雙親委派機制的正確代碼示例,自定義類加載器類詳情,建議創(chuàng)建類加載器時可以選擇不指定父加載器,或者指定父加載器為非空的類加載器,但是不建議指定父加載器為null;

因為如果指定類加載器的父加載器為null的話,那么當加載目標類時,如果目標類的父類是需要委托加載的話,那么它是直接委托給引導類加載器了(因為parent為null),如果這個類是核心類,直接由引導類加載器加載那倒是沒有問題,但如果這個父類是需要擴展類加載器或應用類加載器加載的話,就會出現(xiàn)加載不到的問題。

而如果創(chuàng)建類加載器時指定了父加載器的話,那么肯定是可以做到層層往上委托的,而不是一下子直接委托給引導類加載器,就不會出現(xiàn)類加載不到的問題。

public class MyClassLoader extends ClassLoader{

    //用于指定類加載器要加載的類地址
    private String classPath;

    //無參構(gòu)造方法
    public MyClassLoader(String classPath){
        super();
        this.classPath = classPath;
    }

    //帶參構(gòu)造方法,
    //參數(shù)是ClassLoader parent,用于指定當前類加載器的父加載器
    public MyClassLoader(String classPath, ClassLoader parent){
        super(parent);
        this.classPath = classPath;
    }

    /**
     * 最終會由這個方法來調(diào)用findClass方法
     * 這個方法中體現(xiàn)了類的雙親委派加載
     * 想要打破雙親委派也是在這個方法實現(xiàn)
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
       /**
         * 沿用雙親委派的那一套
         */
        //        return super.loadClass(name, resolve);

        /**
         * 破壞了類加載的雙親委派流程:
         * 自己需要特殊加載的類直接就由本類加載器進行加載;
         * 而核心類就向父加載器進行委托,最終由引導類加載器進行加載。
         *
         * 破壞雙親委派就那么簡單。
         *
         * 需要重寫loadClass方法;
         * 因為雙親委派的邏輯實現(xiàn)就是在ClassLoader的loadClass方法中。
         *
         * 在這種情況下,自定義的類加載器的父加載器在賦值時絕不能賦空值,否則它就沒有父加載器了,就沒法向上委托加載任務了。
         *
         * 可以只重寫loadClass方法而不用去管findClass方法,把findClass的實現(xiàn)邏輯都放到loadClass就行了;
         * 當然也可以同時重寫loadClass和findClass方法。
         *
         */

        //判斷類是否已經(jīng)加載過了,如果加載過了則直接返回即可
        Class<?> loadedClass = findLoadedClass(name);

        //沒加載過的話,需要執(zhí)行加載邏輯
        if (loadedClass == null){
            //如果加載的是目標類,那么直接調(diào)用本類的findClass方法即可,不委托給父加載器
            if (name.startsWith("test.")){
                loadedClass = this.findClass(name);
            } else {
                //否則調(diào)用父類的加載邏輯,使用雙親委派機制進行加載(目標類可能繼承了某些核心類,需要系統(tǒng)類加載器才能加載的)
                return super.loadClass(name, false);
            }
        }

        //如果需要即時解析,則進行解析操作
        if (resolve) resolveClass(loadedClass);

        //返回加載的Class對象
        return loadedClass;

    }

    /**
     * 這個方法與上面那個方法的作用是一樣的,唯一的區(qū)別是,這個方法沒有resolve參數(shù)
     * 默認加載后先不解析
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return this.loadClass(name, false);
    }


    /**
     * 其作用是從磁盤讀取字節(jié)碼文件到內(nèi)存,
     * 實際的類加載流程,最終loadClass方法還是會調(diào)用findClass方法的
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(classPath);
        try {
            /**
             * 讀字節(jié)流,不需要reader
             */
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = in.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }

            byte[] data = baos.toByteArray();

            if (data == null){
                throw new ClassNotFoundException("找不到目標類,加載目標類失敗...");
            } else {
                /**
                 * 將讀取的字節(jié)碼數(shù)據(jù)交給jvm去構(gòu)建成字節(jié)碼對象
                 */
                return defineClass(name, data, 0, data.length);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

}

破壞雙親委派錯誤的代碼示例:

打破java雙親委派模型的錯誤代碼示例

這個代碼示例會報錯:java.lang.Object類找不到。

image

原因:loadClass方法中僅包含了本類實現(xiàn)的加載類的邏輯,但是并沒有包含加載java核心類的邏輯,即沒有將核心類的加載委托給父加載器進行加載,所以導致核心類加載不到,因為任何一個java類最起碼都會繼承java.lang.Object類,如果子類的實現(xiàn)邏輯中完全沒有雙親委托邏輯,核心類的加載最終就到不了引導類加載器,所以就報了找不到Object類的異常。

System.out.println(loadClass.getClassLoader());

類的加載器是什么取決于該類被哪個加載器加載的,如果它是被自定義加載器加載的,那么它的類加載器肯定是自定義的加載器;
但是如果它是被應用類加載器加載的,那么它的加載器就應該是應用類加載器;
同理,對于其他的加載器也是這樣。

因為類加載有委托機制,所以目標類的加載器就不一定是最底層的子加載器了,有可能是它的父加載器或者更上層的加載器。

總之,需要明白的一點就是:哪種加載器加載的目標類,那么目標類的加載器就是哪種。

java雙親委派模型解析

參考:
https://blog.csdn.net/briblue/article/details/54973413

如何破壞雙親委派模型

參考:
https://blog.csdn.net/weixin_43884884/article/details/107719529

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容