java类加载流程之双亲委派与破坏

双亲委派是什么?

1、首先我们看下如何自定义一个类加载器
自定义类加载器需要继承ClassLoader类,并重写loadClass(String name, boolean resolve)、loadClass(String name)和findClass(String name)方法;
自定义类加载器中被重写的loadClass方法将由项目代码调用。

public class MyClassLoader extends ClassLoader{

    //用于指定类加载器要加载的类地址
    private String classPath;

    //无参构造方法
    public MyClassLoader(String classPath){
        super();
        this.classPath = classPath;
    }

    //带参构造方法,
    //参数是ClassLoader parent,用于指定当前类加载器的父加载器
    public MyClassLoader(String classPath, ClassLoader parent){
        super(parent);
        this.classPath = classPath;
    }

    /**
     * 最终会由这个方法来调用findClass方法
     * 这个方法中体现了类的双亲委派加载
     * 想要打破双亲委派也是在这个方法实现
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        //判断类是否已经加载过了,如果加载过了则直接返回即可
        Class<?> loadedClass = findLoadedClass(name);

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

        //如果需要即时解析,则进行解析操作
        if (resolve) resolveClass(loadedClass);

        //返回加载的Class对象
        return loadedClass;

    }

    /**
     * 这个方法与上面那个方法的作用是一样的,唯一的区别是,这个方法没有resolve参数
     * 默认加载后先不解析
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return this.loadClass(name, false);
    }


    /**
     * 其作用是从磁盘读取字节码文件到内存
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(classPath);
        try {
            /**
             * 读字节流,不需要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 {
                /**
                 * 将读取的字节码数据交给jvm去构建成字节码对象
                 */
                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,在外界代码中调用loadClass方法
            Class<?> loadClass = myClassLoader.loadClass("test.Person");
            //打印加载的类是不是目标类
            System.out.println(loadClass.getName());
            //获取当前已加载的Class对象的类加载器是什么
            System.out.println(loadClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}

再分析下ClassLoader类的构造方法:

它有3个构造方法,两个protected的和一个private的;
protected ClassLoader(),无参构造方法 ;
protected ClassLoader(ClassLoader parent),带参构造方法,可以用于指定当前类加载器的父加载器;
private ClassLoader(Void unused, ClassLoader parent) ,类内部使用的构造方法, ClassLoader()和ClassLoader(ClassLoader parent)都是调用的这个构造方法做初始化。

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.
     *
     * 创建自定义类加载器对象时,指定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.
     *
     * 不带参数,默认通过getSystemClassLoader()方法创建类加载器的父加载器
     *
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

ClassLoader()无参构造方法因为不带参数,所以没法通过外界指定当前类加载器的父加载器,在它的默认实现中,它默认通过getSystemClassLoader()方法来指定当前类加载器的父加载器;
可以看到这个方法的返回值是ClassLoader类, 这个方法的作用是创建系统类加载类对象scl,并返回scl对象作为自定义类加载器对象的父加载器

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

private static synchronized void initSystemClassLoader() {
    /**
     * 
     *  sclSet属性是一个boolean类型的静态属性,它的作用是用于判断system classloader是否已经被创建了,
     * 如果已经被创建了,则将sclSet属性的值设置为true,否则它的值还是默认值false
     *
     * 
     * 能够进入这个语句块说明system classloader还没被创建
     */
    if (!sclSet) {

        /**
         * 
         * scl表示system classloader,
         * 如果它不为null,却还重新创建的话,就抛异常
         * 
         */
        if (scl != null)
            throw new IllegalStateException("recursive invocation");

        /**
         * 执行到这里说明scl还没被创建
         * 
         * getLauncher()获取Launcher对象
         * 
         */
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

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

            /**
             * 
             * 通过调用Launcher对象的getClassLoader()方法获取Launcher类定义的ClassLoader对象
             */
            scl = l.getClassLoader();
            try {

                /**
                 * 
                 * 将上面获取的类加载器封装为系统类加载器
                 */
                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);
                }
            }
        }

        //系统类加载器创建完成之后,将sclSet标识设置为true
        sclSet = true;
    }
}

通过上面的分析我们可以知道,initSystemClassLoader()方法涉及到一个非常重要的类Launcher,下面我们要好好分析下Launcher类:

从上面的分析我们知道了initSystemClassLoader()方法会调用getLauncher()方法和getClassLoader()方法,我们来看下代码:

getLauncher()方法的作用是返回Launcher类的Launcher launcher属性,而launcher默认是已经初始化的了,通过new Launcher()构造方法创建了一个Launcher对象

我们再看下Launcher类的构造方法逻辑:

因为initSystemClassLoader()方法还调用了getClassLoader()方法,我们再看下getClassLoader()方法的逻辑:
可以知道它是直接返回了Launcher类的loader对象引用,根据上面的分析我们知道,loader引用指向的是应用类加载器,所以调用Launcher类的getClassLoader()方法得到的是应用类加载器对象

至此,自定义类加载器的创建过程算是分析完成了。

此外,关于Launcher类还有一个需要分析的点:
Launcher类有三个静态内部类需要关注:
1)ExtClassLoader和AppClassLoader,它们都继承自URLClassLoader
2)BootClassPathHolder

ExtClassLoader类

ExtClassLoader类加载器加载的类路径由系统参数java.ext.dirs参数指定,默认是jdk/jre/lib/ext目录下的jar包和.class文件

AppClassLoader类

AppClassLoader类加载器加载的类路径由系统参数java.class.path指定,默认是jdk/jre/lib目录下的某些jar包以及项目编译目录target\classes下的.class文件

还有一个是引导类加载器加载的类路径是由系统参数sun.boot.class.path指定的,默认是jdk/jre/lib目录下的某些jar包以及jdk/jre/classes目录下的.class文件

根据上面的分析,我们知道自定义类加载器、应用类加载器、扩展类加载器和引导类加载器直接的关系如下图所示:

2、上面已经分析完了如何创建一个类加载器,如何指定类加载器的父加载器以及分析它的父加载器是什么:appClassLoader?null?看自己实例化了加载器时的选择。

基于上述分析,我们已经可以知道一个类加载器对象是怎么创建的了,接下来我们需要分析类加载器是如何加载类信息的:

重点分析ClassLoader类的loadClass方法逻辑:

ClassLoader类有loadClass(String name) 和 loadClass(String name, boolean resolve)方法,可以看到loadClass(String name)在ClassLoader类中实际上是调用了loadClass(String name, boolean resolve)方法的,它的resolve参数默认设置为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
            //这行代码的逻辑是判断指定类是否之前已经加载过了,内存中是否已经存在了
            //如果已经加载过了,则不需要重新加载,直接返回其Class对象即可
            Class<?> c = findLoadedClass(name);
            
            // 结果为null,说明之前还没加载过,通过这个逻辑进行加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                        //这里表示如果当前类加载器的父加载器不为null,则调用父加载器的loadClass方法来加载目标类
                        //重点来了,java的双亲委派机制在这里就体现出来了:
                        //如果当前类加载器的父加载器不为null,就通过父加载器来加载器目标类,即将加载目标类的任务委托给了父加载器来执行
                        //这里有递归调用的意味了:
                        //当前类加载器委托它的父加载器来加载目标类,它父加载器也会判断下自己有没有父加载器,如果有的话,也会委托给自己的父加载器来执行加载任务,就这样层层地委托下去,直到父加载器为null
                    if (parent != null) {
                        //如果父加载器为null,就将加载目标类的任务委托给引导类加载器来加载,也就是说通过双亲委派机制的层层委托之后,最终接收这个加载任务来执行的是引导类加载器
                        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
                }

                //然后这里判断下引导类加载器是否加载目标类成功了,如果成功则返回加载类信息得到的字节码对象,
                //否则,通过本类加载器的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);
            }

            //返回目标类的字节码对象,可能为null
            return c;
        }
    }

通过上述分析,可以了解到,当前类加载器会将加载目标类的任务先委托给它的父加载器,然后父加载器也是同理先把加载目标类的任务委托给它的父加载器,...,就这样层层委托,最后委托给引导类加载器,如果引导类加载器加载不到的话(因为它只能加载特定目录下的,不在那个目录下就加载不到),就由当前那个类加载器自己的加载逻辑findClass来加载,然后返回。

由于上述的委派给父类加载的流程其实是一个递归调用逻辑,所以就存在层层返回的情况,如果上一层返回后,没有加载到目标类,那么就返回null;

当返回结果是null,那么就需要调用当前的类加载器的加载逻辑findClass来加载目标类,如果加载到了目标类,则直接层层返回即可。

4、上述过程解析了类加载器加载目标类的思路,分析我们常听说的双亲委派机制到底是什么,如何实现委派的。

我们知道了双亲委派是什么样的,那么我们应该如何破坏这个机制呢?

破坏双亲委派的正确思路:
因为一个最基本的java类最起码都要继承java.lang.Object类,而这个类是核心类,核心类是只能由引导类加载器进行加载,所以正确的实现逻辑应该是自己想要特殊加载的直接由自定义加载器加载,不向父加载器委托,但是呢实现逻辑里必须兼容的一点是对java核心类使用双亲委派机制,将核心类交由引导类加载器进行加载,以防直接使用自定义类加载器的话出现最基本的java.lang.Object类加载不到的情况

破坏双亲委派机制的正确代码示例,自定义类加载器类详情,建议创建类加载器时可以选择不指定父加载器,或者指定父加载器为非空的类加载器,但是不建议指定父加载器为null;

因为如果指定类加载器的父加载器为null的话,那么当加载目标类时,如果目标类的父类是需要委托加载的话,那么它是直接委托给引导类加载器了(因为parent为null),如果这个类是核心类,直接由引导类加载器加载那倒是没有问题,但如果这个父类是需要扩展类加载器或应用类加载器加载的话,就会出现加载不到的问题。

而如果创建类加载器时指定了父加载器的话,那么肯定是可以做到层层往上委托的,而不是一下子直接委托给引导类加载器,就不会出现类加载不到的问题。

public class MyClassLoader extends ClassLoader{

    //用于指定类加载器要加载的类地址
    private String classPath;

    //无参构造方法
    public MyClassLoader(String classPath){
        super();
        this.classPath = classPath;
    }

    //带参构造方法,
    //参数是ClassLoader parent,用于指定当前类加载器的父加载器
    public MyClassLoader(String classPath, ClassLoader parent){
        super(parent);
        this.classPath = classPath;
    }

    /**
     * 最终会由这个方法来调用findClass方法
     * 这个方法中体现了类的双亲委派加载
     * 想要打破双亲委派也是在这个方法实现
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
       /**
         * 沿用双亲委派的那一套
         */
        //        return super.loadClass(name, resolve);

        /**
         * 破坏了类加载的双亲委派流程:
         * 自己需要特殊加载的类直接就由本类加载器进行加载;
         * 而核心类就向父加载器进行委托,最终由引导类加载器进行加载。
         *
         * 破坏双亲委派就那么简单。
         *
         * 需要重写loadClass方法;
         * 因为双亲委派的逻辑实现就是在ClassLoader的loadClass方法中。
         *
         * 在这种情况下,自定义的类加载器的父加载器在赋值时绝不能赋空值,否则它就没有父加载器了,就没法向上委托加载任务了。
         *
         * 可以只重写loadClass方法而不用去管findClass方法,把findClass的实现逻辑都放到loadClass就行了;
         * 当然也可以同时重写loadClass和findClass方法。
         *
         */

        //判断类是否已经加载过了,如果加载过了则直接返回即可
        Class<?> loadedClass = findLoadedClass(name);

        //没加载过的话,需要执行加载逻辑
        if (loadedClass == null){
            //如果加载的是目标类,那么直接调用本类的findClass方法即可,不委托给父加载器
            if (name.startsWith("test.")){
                loadedClass = this.findClass(name);
            } else {
                //否则调用父类的加载逻辑,使用双亲委派机制进行加载(目标类可能继承了某些核心类,需要系统类加载器才能加载的)
                return super.loadClass(name, false);
            }
        }

        //如果需要即时解析,则进行解析操作
        if (resolve) resolveClass(loadedClass);

        //返回加载的Class对象
        return loadedClass;

    }

    /**
     * 这个方法与上面那个方法的作用是一样的,唯一的区别是,这个方法没有resolve参数
     * 默认加载后先不解析
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return this.loadClass(name, false);
    }


    /**
     * 其作用是从磁盘读取字节码文件到内存,
     * 实际的类加载流程,最终loadClass方法还是会调用findClass方法的
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        File file = new File(classPath);
        try {
            /**
             * 读字节流,不需要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 {
                /**
                 * 将读取的字节码数据交给jvm去构建成字节码对象
                 */
                return defineClass(name, data, 0, data.length);
            }

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

    }

}

破坏双亲委派错误的代码示例:

打破java双亲委派模型的错误代码示例

这个代码示例会报错:java.lang.Object类找不到。

image

原因:loadClass方法中仅包含了本类实现的加载类的逻辑,但是并没有包含加载java核心类的逻辑,即没有将核心类的加载委托给父加载器进行加载,所以导致核心类加载不到,因为任何一个java类最起码都会继承java.lang.Object类,如果子类的实现逻辑中完全没有双亲委托逻辑,核心类的加载最终就到不了引导类加载器,所以就报了找不到Object类的异常。

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

类的加载器是什么取决于该类被哪个加载器加载的,如果它是被自定义加载器加载的,那么它的类加载器肯定是自定义的加载器;
但是如果它是被应用类加载器加载的,那么它的加载器就应该是应用类加载器;
同理,对于其他的加载器也是这样。

因为类加载有委托机制,所以目标类的加载器就不一定是最底层的子加载器了,有可能是它的父加载器或者更上层的加载器。

总之,需要明白的一点就是:哪种加载器加载的目标类,那么目标类的加载器就是哪种。

java双亲委派模型解析

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

如何破坏双亲委派模型

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容