双亲委派模型

前言

今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。
广告时间:先点赞,先收藏,转粉不转路。

问题

大家思考一下这些问题:

  1. 为什么不能定义java.lang.Object的Java文件?
  2. 在多线程的情况下,类的加载为什么不会出现重复加载的情况?
  3. 以下代码,JVM是怎么初始化注册MySQL的驱动Driver?
         Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "root");

解答

以上这些问题,其实都和双亲委派模型有关,双亲委派模型,在面试中,是非常热的考察点。

首先,我们得知道什么是双亲委派模型?
图片摘自网络
简单点说,所谓的双亲委派模型,就是加载类的时候,先请求其父类加载器去加载,如果父类加载器无法加载类,再尝试自己去加载类。如果都没加载到,就抛出异常。

现在让我们回到第一个问题:为什么不能创建java.lang.Object的Java文件?

即使我们已经定义了java.lang.Object的Java文件,但其实也无法加载,因为java.lang.Object已经被启动类加载器加载了。

你可能会问:为什么JVM要使用双亲委派模型来加载类?

一,性能,避免重复加载;二,安全性,避免核心类被修改。

第一点,没法好说的。说说第二点安全性吧,你试想一下。假设我现在创建一个java.lang.Object的Java文件,然后在里面植入一些病毒木马,或者写一些死循环在构造方法中。对JVM来说,这是致命的。

接下来,简单介绍一下各种类加载器:

  • 启动类加载器:它不是一个Java类,是C++写的。主要负责JDK的核心类库,比如rt.jar,resource.jar等类库。启动类加载器完全是JVM自己控制的,开发人员是无法访问的。
  • 扩展类加载器:是一个继承ClassLoader类的Java类,负责加载{JAVA_HOME}/jre/lib/ext/目录下的所有jar包
  • 应用程序类加载器:是一个继承ClassLoader类的Java类,负载加载classpath目录下的所有jar和class文件,基本上你写的类文件,都是被应用程序类加载器加载的。

可以用以下代码,打印出三个类加载器的加载的文件。

public class TestEnvironment {
    public static void main(String[] args) {
        //启动类加载器
        System.out.println("1"+System.getProperty("sun.boot.class.path"));
        //扩展类加载器
        System.out.println("2"+System.getProperty("java.ext.dirs"));
        //应用类加载器
        System.out.println("3"+System.getProperty("java.class.path"));
    }
}

补充一下:三个类加载器的关系,不是父子关系,是组合关系。

接下来我们看看类加载器的加载类的方法loadClass

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //看,这里有锁
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //去看看类是否被加载过,如果被加载过,就立即返回
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //这里通过是否有parent来区分启动类加载器和其他2个类加载器
                    if (parent != null) {
                        //先尝试请求父类加载器去加载类,父类加载器加载不到,再去尝试自己加载类
                        c = parent.loadClass(name, false);
                    } else {
                        //启动类加载器加载类,本质是调用c++的方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果父类加载器加载不到类,子类加载器再尝试自己加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //加载类
                    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);
            }
            return c;
        }
    }

总结一下loadClass方法的大概逻辑:

  1. 首先加锁,防止多线程的情况下,重复加载同一个类
  2. 当加载类的时候,先请求其父类加载器去加载类,如果父类加载器无法加载类时,才自己尝试去加载类。
    图片摘自网络

上面的源码解析,可以回答问题:在多线程的情况下,类的加载为什么不会出现重复加载的情况?

好,目前我们已经解决2个问题了。

接下来

就要开始破坏双亲委派模型了。首先声明哈,双亲委派模型,JVM并没有强制要求遵守,只是说推荐

我们来总结一下,双亲委派模型就是子类加载器调用父类加载器去加载类。那如何来破坏呢?可以使得父类加载器调用子类加载器去加载类,这便破坏了双亲委派模型。

在讲解MySQL的驱动前,先补充一个知识点:

Class.forName() 与 ClassLoader.loadClass() 两种类的加载方式的区别

Class.forName()

  • 实质是调用原生的forName0()方法

  • 保证一个Java类被有效得加载到内存中;

  • 类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;

  • 默认会使用当前的类加载器来加载对应的类(先记住这个特点,下面会用到)

    ClassLoader.loadClass()

  • 实质是启动类加载器进行加载

  • 与Class.forName()不同,类不会被初始化,只有显式调用才会进行初始化。

  • 类会被加载到内存中

  • 提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。

我们继续讲一下关于MySQL的驱动,我们列举2种情况进行对比理解:

第一种:不破坏双亲委派模型

自定义的Java类

         // 1.加载数据访问驱动
        Class.forName("com.mysql.jdbc.Driver");
        //2.连接到数据"库"上去
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

分析一下这2行代码:
第一行:进行类加载,还记得上面说过Class.forName()会使用当前的类加载器来加载对应的类。当前的类,就是用户写的Java类,用户写的Java类使用应用程序类加载器加载的。那现在问题就是应用程序类加载器是否能加载com.mysql.jdbc.Driver这个类,答案是可以的。因此这种方式加载类,是不会破坏双亲委派模型的。
第二行:就是通过遍历的方式,来获取MySQL驱动的具体连接。

第二种:破坏双亲委派模型
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

这和不破坏双亲委派模型的代码有啥区别:就是少了Class.forName("com.mysql.jdbc.Driver")这一行。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

因为少了Class.forName(),因为就不会触发Driver的静态代码块,进而少了注册的过程。

现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:

第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第二,加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载。

好了,问题来了,现在这个调用者是DriverManager,加载DriverManager在rt.jar中,rt.jar是被启动类加载器加载的。还记得上面Class.forName()会使用当前的类加载器来加载对应的类。也就是说,启动类加载器会去加载com.mysql.jdbc.Driver,但真的可以加载得到吗?很明显不可以,为什么?因为om.mysql.jdbc.Driver肯定不在<java_home>/lib下,所以肯定启动类加载器是无法加载com.mysql.jdbc.Driver这个类。这就是双亲委派模型的局限性:父类加载器无法加载子类加载器路径中的类。</java_home>

问题我们定位出来了,接下来该如何解决?

我们分析一下,列出来:

  • 一,可以肯定com.mysql.jdbc.Driver,只能由应用程序类加载器加载。
  • 二,我们需要使用启动类加载器去获取应用程序类加载器,进而通过应用程序类加载器去加载com.mysql.jdbc.Driver。

那么问题就变为了:如何让启动类加载器去获取应用程序类加载器?

为了解决上述的问题,我们需要引入一个新概念:线程上下文类加载器

线程上下文类加载器(ThreadContextClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。

线程上下文加载器:可以让父类加载器通过调用子类加载器去加载类。

这里得注意一下:我们之前定义的双亲委派模型是:子类加载器调用父类加载器去加载类。现在相反了,换句说,其实已经破坏了双亲委派模型。

如果你看到这里,相信你已经会解答问题3了吧。

今天就到这里结束了!明天见!

参考资料

《深入理解JAVA虚拟机》
低情商的大仙——以JDBC为例谈双亲委派模型的破坏

絮叨

非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力!

全部评论

相关推荐

11-08 16:53
门头沟学院 C++
投票
滑模小马达:第三个如果是qfqc感觉还行,我签的qfkj搞电机的,违约金也很高,但公司感觉还可以,听说之前开过一个试用转正的应届生,仅供参考。
点赞 评论 收藏
分享
有趣的牛油果开挂了:最近这个阶段收到些杂七杂八的短信是真的烦
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务