【类加载】进阶知识点与思考

2.3进阶知识点与思考

2.3.1 可以被破坏的双亲委派模型

双亲委派模型一种有效的类加载模型,绝大多数类加载器采用这一模型实现,但是仍有一些特殊情况导致模型被破坏。
 
由于在双亲委派模型实现前,JDK已经提供自定义类加载的实现,导致双亲委派模型存在被破坏的机会。在使用自定义类加载器时,通过在loadClass()中编写加载逻辑实现。为了避免自定义类加载器使用上述方式,JDK在java.lang.ClassLoader中添加了findClass()方法,并要求用户重写这一方法。
 
双亲委派模型的第二次被破坏是由于其本身的性质。如果基础类在被加载时又要调用回用户的代码,启动类加载器将无法识别这些代码。为了解决这个问题,引入了线程上下文加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建时还未设置,他将从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那这个类加载器默认就是应用程序类加载器。这里简单介绍下JDBC中破坏双亲委派机制的相关内容。
 
为什么JDBC要破坏双亲委派模型?这是因为在一些场景下下父类加载器需要委托子类加载器去加载.class文件。Driver接口(DriverManager)仅仅在JDK中存在定义,而没有具体实现。具体实现由各个数据库厂商自行提供,但是不能把所有厂商提供的驱动代码全放到JDK的lib目录吧。例如如下代码:

Class clz = Class.forName("java.sql.Driver");
Driver d = (Driver) clz.newInstance();

 
BootstrapClassLoader将尝试加载,但是java.sql.Driver仅仅是一个接口,无法进行实例化,所以执行必然失败。如果一定执行成功,可以将mysql-connector-java.jar丢到JDK的lib目录下,但是这种方式过于硬核,扩展性和优雅性不足。DriverManager的加载可由BootstrapClassLoader实现,但是数据库厂商提供的诸如mysql-connector-.jar无法由BootstrapClassLoader加载。而通过线程上下文类加载器可实现将BootstrapClassLoader无法识别或加载的类转交AppClassLoader进行加载。大家可以自己再执行下面代码观察结果。

public static void main(String[] args){
    Connection connection = null;
    try {
        connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "xxx", "xxxxx");
    } catch (SQLException e) {
        e.printStackTrace();
    }
    System.out.println(connection.getClass().getClassLoader());
    System.out.println(Thread.currentThread().getContextClassLoader());
    System.out.println(Connection.class.getClassLoader());
}

 
下面我们再关注一下getConnection()的实现

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {

    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    // 同步块处理
    synchronized(DriverManager.class) {
        // callerCL为空说明时启动类加载器,但是启动类加载器不能识别rt.jar之外的类
        // 为了完成加载,可以从线程上下文中获取应用类加载器
        if (callerCL == null) {
          // 获取线程上下文中类加载器
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }
    println("DriverManager.getConnection(\"" + url + "\")");
    SQLException reason = null;

    for(DriverInfo aDriver : registeredDrivers) {
          // isDriverAllowed中实现mysql-connector-java.jar的加载
        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 (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");
}

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;
}

 
双亲委派模型的第三次被破坏是由于动态性变态追求导致,实现动态性的好处例如代码热替换、模块热部署。动态性固然有许多好处,但是往往会在性能或其他场景上存在一定缺陷。OSGi是Java模块化标准,允许自定义类加载器,并且每一个程序模块都有一个类加载器。在JDK9之前,基础类集中在rt.jar包内。然而过度集中缺违反了单一职责原则,这将导致编译时会将很多无用的类也一并打包。JDK9中引入了模块化的思想,对rt.jar中的模块进行解耦,分为数十个模块单独管理。编译时也只引入用到的模块。下面以一段伪代码对JDK9中的加载过程进行描述:

Class<?> c = findLoadedClass(cn);
  if (c == null) {
     // 定位类所属模块
     LoadedModule loadedModule = findLoadedModule(cn);
     if (loadedModule != null) {
        // 获取所属模块的类加载器
        BuiltinClassLoader loader = loadedModule.loader();
        // 执行类加载
        c = findClassInModuleOrNull(loadedModule, cn);
     } else {
        // 降级执行双亲委派
     if (parent != null) {
       c = parent.loadClassOrNull(cn);
     }
   }

 
显然,双亲委派在JDK9中成为了一种后备加载方式。这种模块化方式应用在类加载场景更高效,也可以减少不必要的打包。这里仅简单对OSGi、模块化系统进行介绍,详细内容感兴趣的同学可以自行深入理解,例如深入理解OSGi:Equinox原理、应用与最佳实践。
 

2.3.2 类加载部分源码简析

本节对类加载涉及的部分源码进行分析,不涉及C/C++实现的Bootstrap层类加载器。

首先通过代码打印出各个类加载器:

public class Application {

    public static void main(String[] args) {

        Application app = new Application();
        ClassLoader loader = app.getClass().getClassLoader();

        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
        System.out.println(ClassLoader.getSystemClassLoader());
    }
}
// 代码输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@61bbe9ba
null
sun.misc.Launcher$AppClassLoader@18b4aac2

通过输出我们不难发现,AppClassLoader和ExtClassLoader都是类Launcher类下的静态内部类。这个Launcher类是Java程序执行的入口。启动Java应用时首

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java面试必问JVM考点精讲 文章被收录于专栏

“挨踢”行业行情日益严峻,企业招聘的门槛也随之越来越高,大厂hc少之又少。 庞大的知识体系下,不知道学什么、怎么学? 面试高频考点是什么、怎么回答才能得到面试官的青睐? 作为后端求职者,在Java的道路上越走越宽。 本专刊则针对Java面试考点上,精讲JVM知识点,为大家的大厂求职路保驾护航! 针对如今校招痛点,深入详解JVM知识考点,列出高频真题并详细解答!探索JVM精髓!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务