Java_JVM_Java的双亲委派模型 与 破坏双亲委派模型实例

 

参考文章:

1.双亲委派模型的破坏(JDBC例子)

https://blog.csdn.net/awake_lqh/article/details/106171219

2.面试官:说说双亲委派模型?

https://baijiahao.baidu.com/s?id=1633056679004596814&wfr=spider&for=pc

3.【JVM】浅谈双亲委派和破坏双亲委派

https://www.cnblogs.com/joemsu/p/9310226.html

 

   在我们的面试过程中,免不了会被问到 JVM 相关的知识。其中双亲委派模型 就是一个较为经常被考察的点。下面对这个点做一个整理。

 

类的生命周期

  类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括, 加载,验证,准备,解析,初始化,使用 和 卸载 7个步奏。

 

类的生命周期

 加载 -》 验证 -》准备 -》解析 -》初始化 -》使用  -》卸载

 

以下5个阶段的顺序是确定的

 加载 -》验证 -》准备 -》 初始化 -》卸载

 

类加载的全过程 5个阶段

 加载 -》 验证 -》准备 -》解析 -》初始化 

其中 验证-》准备-》解析 又被统称为连接,所以 类加载又可以称为 加载,连接,初始化 3个阶段。

 

验证

验证阶段主要完成以下4个阶段的检验动作

1.文件格式验证

2.元数据验证

3.字节码验证

4.符号引用验证

 

准备

 准备阶段正式为类变量分配内存,并设置类变量的初始值的阶段,这些变量所使用的内存都将是在方法去中进行分配的。

Tips:

 1.这时候进行内存分配的仅包括类变量(被 static 修饰的变量)

 2.这里说的初始值 通常情况 下是 数据类型的零值,例如 public static int value = 123;  准备后的初始值为0,而不是123

 

初始化的5种情况

   有且只有 以下5种情况, 必须立即对类进行 “初始化”。(而加载,验证,准备自然需要在此次之前开始)

1.遇到 new, getstatic , putstatic 或 invokestatic 这 4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

2.使用 java.lang.reflect 包的方法对类 进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类 (包含 main() 方法的哪个类),虚拟机会先初始化这个主类。

5.当使用 JDK 1.7 的动态语言支持时,如果一个 Java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic , REF_putStatic, REF_invoke 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对 HotSpot 虚拟机,可以通过 -XX:+TraceClassLoading 参数观察此操作是否导致子类的加载。

 

 

双亲委派模型

 

类加载器引申的问题

     在Java中任意一个类都是由 这个类本身加载这个类的类加载器来确定 这个类在JVM中的唯一性。也就是你用你A类加载器加载的com.aa.ClassA 和 你B 类加载器加载的com.aa.ClassA它们是不同的,也就是用instanceof这种对比都是不同的。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。

 

Java 的类加载器

  Java 提供了 3种类加载器 , 启动类加载器,扩展类加载器 和 应用程序类加载器

   启动类加载器 (Bootstrap ClassLoader ):  它是属于虚拟机自身的一部分,用C++实现的。 这个类加载器类 负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库 即使放到lib 目录中也不会被加载)类库加载到虚拟机内存中。

 

  扩展类加载器(Extension ClassLoader): Java 实现的,独立于虚拟机,主要负责加载\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。开发者可以直接使用扩展类加载器。

 

   应用程序类加载器(Application ClassLoader)它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器,  那 它 Application ClassLoader  就是我们程序中的默认加载器。

 

 

类加载器的层次模型

那么有那么多的类加载器,它们之前的层级关系是怎样的呢。它们之间是使用的双亲委派模型,如下图

 

Java_JVM_Java的双亲委派模型 与 破坏双亲委派模型实例_第1张图片

 

  如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

这里有几个流程要注意一下:

  1. 子类先委托父类加载
  2. 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
  3. 子类在收到父类无法加载的时候,才会自己去加载

 

双亲委派的实现

 双亲委派对保证Java 程序运行的稳定性很重要,实现却很简单,实现代码都在 java.lang.ClassLoader 的 loadClass() 方法中

Java_JVM_Java的双亲委派模型 与 破坏双亲委派模型实例_第2张图片

/**
     * Loads the class with the specified binary name.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * 
    * *
  1. Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded.

  2. * *
  3. Invoke the {@link #loadClass(String) loadClass} method * on the parent class loader. If the parent is null the class * loader built-in to the virtual machine is used, instead.

  4. * *
  5. Invoke the {@link #findClass(String)} method to find the * class.

  6. * *
* *

If the class was found using the above steps, and the * resolve flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting Class object. * *

Subclasses of ClassLoader are encouraged to override {@link * #findClass(String)}, rather than this method.

* *

Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock getClassLoadingLock} method * during the entire class loading process. * * @param name * The binary name of the class * * @param resolve * If true then resolve the class * * @return The resulting Class object * * @throws ClassNotFoundException * If the class could not be found */ 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 { if (parent != 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 } 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; } }

 

双亲委派的一些思考

  1.Object 类复写,还是加载到 系统类的Object, 就是由双亲委派模型保证的。

 

 

 

破坏双亲委派模型

   双亲委派模型有一系列的优势,还是需要去破坏双亲委派模型。比如 :基础类去调用回用户的代码。

SPI 破坏双亲委派

  具体的例子:以Driver 接口为例,由于Driver 接口定义在JDK 当中,其实现由各个数据库的服务商来提供,比如 mysql 的就写了 MySQL Connector , 那么为题来了,DriverManager(也由JDK 提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,

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

  Java 中涉及SPI 的加载动作都采用这种方式,例如JNDI, JDBC,JCE,JAXB,JBI 等

OSGi 热部署类加载机制 破坏双亲委派

    双亲委派第三次被破坏是基于程序动态性导致的,如代码热替换(HotSwap), 模块热部署 (Hot Deployment)

    目前OSGi 已经成了业界 事实上的 Java 模块化标准。其实先模块化热部署的关键是自定的类加载机制。每一个程序模块 (OSGi 称为 Bundle)都有一个自己的类加载器,当需要更换一个Bundle 时,就把 Bundle 连同类加载器一起替换掉以实现代码的热替换。

 

JDBC 破坏双亲委派的例子

package thread.classLoad;

import java.sql.Connection;

/**
 * Created by szh on 2020/6/15.
 */
public class TestJDBC {

    public static void main(String[] args) throws Exception{
        String url = "jdbc:mysql://localhost:3306/testdb";
        Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

    }

}

追踪 getConnection 方法

    @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

追踪 getConnection 方法

 //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

 

 

获取线程上下为类加载器

callerCL = Thread.currentThread().getContextClassLoader();

isDriverAllowed对于mysql连接jar进行加载

isDriverAllowed(aDriver.driver, callerCL))

  isDriverAllowed将传入的Thread.currentThread().getContextClassLoader();拿到的应用类加载器用去Class.forName加载我们mysql连接jar,这样子就可以加载到我们自己的mtsql连接jar

    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }


为什么必须要破坏?

    DriverManager::getConnection 方法需要根据参数传进来的 url 从所有已经加载过的 Drivers 里找到一个合适的 Driver 实现类去连接数据库.
    Driver 实现类在第三方 jar 里, 要用 AppClassLoader 加载. 而 DriverManager 是 rt.jar 里的类, 被 BootstrapClassLoader 加载, DriverManager 没法用 BootstrapClassLoader 去加载 Driver 实现类(不再lib下), 所以只能破坏双亲委派模型, 用它下级的 AppClassLoader 去加载 Driver.
 

 

 

 

 

你可能感兴趣的:(Java,JVM)