我曾觉得我了解双亲委派|8月更文挑战

最近面试被问到双亲委派,对于双亲委派和破坏双亲委派的机制以前本身在《深刻理解Java虚拟机》中了解过,当时以为挺简单的一个概念,可是面试官仔细追问下去发现本身这块的只是仍是存在一些误区,当时这一个问题可能聊了有20分钟,固然面试后天然就没有下文了😂。java

故事要从这张双亲委派模型的类UML图提及: image.png面试

  • 误区1:当时看了类加载器的模型图,觉得启动类加载器、扩展类加载器、应用程序类加载器与自定义累加器之间是继承关系,其实“父加载器”与“子加载器”是组合关系,这点其实和父子加载器的命名给人带来的主观想法相悖;
  • 误区2:没有理清findClass()loadClass()之间的关系和这两个方法具体的做用,破坏双亲委派机制与遵循双亲委派机制与这两个方法的相关性;
  • 误区3:由于平时工做中没怎么使用过ClassLoader,没有理清Java中ClassLoader类与Bootstrap Class LoderExtension Class LoaderApplication Class Loader之间的关系;错误的按照类加载器模型图,认为本身实现类加载器须要继承应用程序类加载器以实现自定义类加载器。

误区造成的最主要缘由是当时本身看《深刻理解Java虚拟机》的时候,其实仍是处于知识储备、知识之间的关联性还不够到位的状态,所以今天从新进行梳理一下~数据库

双亲委派模型

什么是双亲委派模型? 双亲委派模型定义了加载器加载类的方式,即当一个加载器收到加载一个类的请求时,首先会委托给父加载器进行处理加载,只有当其父加载器没法加载时,当前加载器才会进行加载;缓存

那么何时会触发类加载器进行类的加载呢? 当在程序中出现对于一个类的主动引用时,若是当前类还没有被加载到方法区中时,会触发对引用类的加载;安全

触发类的加载后类加载器须要作些什么? 虚拟机经过类加载器在Java类加载阶段须要完成如下三件事:markdown

  • 经过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所表明的的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问入口。

那么何时父加载器没法加载一个类呢? 由于每一个加载器加载负责加载的类都是不一样的:数据结构

  • 启动类加载器: 主要加载<JAVA_HOME>\lib目录下的类,并经过文件名匹配,如rt.jartools.jar这类Java核心类库;这个类经过C++语言进行实现,是虚拟机的一部分;
  • 扩展类加载器: 主要加载<JAVA_HOME>\lib\ext目录下的扩展类;经过Java语言进行实现,对应的类为sun.misc.Launcher$ExtClassLoader
  • 应用程序类加载器: 主要加载用户类路径(classpath)下的全部类库;经过Java语言进行实现,对应的类为sun.misc.Launcher$AppClassLoader,能够经过ClassLoader类中的getSystemClassLoader()方法获取应用程序类加载器的引用,而且应用程序中若是没有自定义类加载器,则会使用这个默认的类加载器;

双亲委派模型实现

JDK1.2以后,经过ClassLoader::loadClass()实现双亲委派模型,而且咱们使用ClassLoader时通常也会经过loadClass()方法进行类的加载:app

public abstract class ClassLoader {
    // 当前ClassLoader依赖的父加载器,注意父加载器与当前加载器是组合的关系!
    private final ClassLoader parent;
    
    // 在构建classLoader时,默认会将系统类加载器AppClassLoader做为当前classLoader的父加载器
    // getSystemClassLoader()默认返回AppClassLoader
    // 而AppClassLoader的父加载器也是在初始化时设置为 PlatformClassLoader
    protected ClassLoader() {
        this(checkCreateClassLoader(), null, getSystemClassLoader());
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 首先检查是否已经加载过当前类
            Class<?> c = findLoadedClass(name);
            // 2. 未加载过当前类则进行加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2.1 父加载器不为空,则先使用父加载器进行加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 2.2 父加载器为空,则使用启动类加载器进行加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ignore未找到当前类的异常,说明父加载器加载失败
                }
                
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 2.3 经过当前类加载器进行加载,注意此处是调用了findClass()方法
                    // 而不是像父类加载器经过loadClass()进行加载
                    // 所以若是要遵循双亲委派机制,子加载器须要经过覆盖findClass()方法实现本身的加载逻辑
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}
复制代码

为何要遵循双亲委派模型?

主要缘由在于经过双亲委派模型组织类加载器之间的关系,使得在Java中的类随着它的类加载器也具有了一种带优先级的层次关系,而这种关系保证了类加载的安全性maven

如用户本身定义了一个Object类,那么按照双亲委派模型,首先会经过启动类加载器进行Object类的加载,则天然会加载在<JAVA_HOME>\lib目录下的Obejct.Class,而不会加载用户在自身classpath下定义的Object类,从而保证了Java核心类库的类不会被错误的加载。ide

破坏双亲委派模型

遗憾的是,双亲委派模型在面对复杂的世界时,并不能完美的解决全部问题:

上层组件依赖下层组件的问题:

比较常见的是JDBC,由于咱们项目使用的数据库多是OracleMySQLSQL Server等,而不一样的数据库由于使用方式的不一样提供了不一样的JDBC Driver,从而使得应用代码能够经过调用JDBC Driver Interface对不一样数据进行增删改查操做;

Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
复制代码

可是用于获取数据库链接的DriverManager类是位于rt.jar下的,按照双亲委派模型理应经过启动类加载器进行加载,可是这个类又会由于项目使用的数据库类型依赖不一样的第三方实现的Driver类,而第三方的是实现类通常是放在用户类路径(classpath)下的,启动类加载器天然没法加载;

JDK1.6经过引入基于ThreadContextClassLoader(线程上下文类加载器)实现的ServiceLoader这个特殊的类加载器用来解决这个问题:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 线程上下文类加载器默认为AppClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 经过AppClassLoader加载上层组件中依赖的用户classpath下的包
    return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
复制代码

可是线程上下文使用不当可能会致使内存泄漏的问题,具体能够参考这篇文章:线程上下文类加载器ContextClassLoader内存泄漏隐患

缺少隔离性问题:

对于在一个JVM上会运行多个Web容器的Tomcat,类可见性若是处理不够稳当,会致使Web容器间缺少合理的隔离性从而带来安全性和容器间依赖组件版本冲突以及没法支持类热更的问题;

  • 安全性问题: 按照双亲委派机制,对于同一个JVM下CLASSPATH指定路径下的全部类和库,都会经过AppClassLoader进行加载,所以CLASSPATH路径下的类能够被全部运行在JVM上的Web容器经过AppClassLoader类加载器进行加载、使用;可是对于运行在JVM上的每一个Web应用(servlet容器),容器中的servlet应该只能够访问自身WEB-INF/classesWEB-INF/lib目录下的类;而若是经过AppClassLoader类加载器加载servlet,那么servlet即可以经过AppClassLoader访问到CLASSPATH下的其余非本容器的类,而这显然违反了servlet容器须要的类隔离性;

  • 版本冲突: 项目的多个模块间可能存在对于同一个第三方组件不一样版本的依赖,而按照双亲委派模型,若是使用AppClassLoader做为类加载器加载各个模块的依赖类,会致使在CLASSPATH路径下只有一个版本的第三方组件会被加载;PS:相似的还有经过maven直接或间接引入了一个包的多个版本致使版本冲突的问题,也是经过自定义类加载器模型进行解决;

  • 没法支持热更: 由于按照双亲委派机制,不一样模块之间共同依赖了同一个AppClassLoader,使得模块之间对于所加载的类存在耦合关系,即咱们不能够随便卸载并替换一个类,由于并不知道是否还有其余模块依赖这个版本的第三方组件;

解决方法:

为了解决容器间缺少隔离性带来的一系列问题,Tomcat自定义了自身的类加载器模型:

image.png

Tomcat容器经过复写ClassLoader::loadClass()方法自定义了破坏双亲委派模型的WebappClassLoader类加载器,并为每一个Web应用都关联了一个WebAppClassLoader,从而实现了Web应用间特殊依赖类的隔离性即每一个应用首先会经过自身该类加载器加载自身WEB-INF/classesWEB-INF/lib目录下依赖的类库, 只有当加载不到所须要的类时才会交由上层类加载器进行加载(破坏了双亲委派首先交由父类进行加载的规则);

可是Tomcat的每一个Web应用间可能也会存在共性的一些依赖,如Servlet规范相关包以及一些工具类包,所以Tomcat的类加载器模型经过父加载器SharedClassLoader进行加载在Web应用间共享的jar包。

而按照该类加载器模型,实现热更只须要动态替换掉Web容器的WebAppClassLoader便可,避免了重启整个Java项目带来的损耗,并不须要担忧被替换掉的类加载器所加载的类会不会被运行在同一个JVM上的Web应用引用的问题。

具体代码实现

主要在复写的loadClass()方法中进行自身加载模型的定义,Tomcat自定义类加载器除了上述中定义类加载器模型的目的以外,还有实现对于已加载类的缓存、类的预载入这两个方面;

public abstract class WebappClassLoaderBase extends URLClassLoader {
    // ClassLoader对于已加载类的内存缓存
    protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

    @Override
    // @param: name,须要加载的类的全限定名
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = null;

            // 1. 缓存查询
            // 1.1 WebAppClassLoader自身Map缓存
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                return clazz;
            }

            // 1.2 查询JVM缓存
            clazz = findLoadedClass(name);
            if (clazz != null) {
                return clazz;
            }

            // 2. 开始类加载
            String resourceName = binaryNameToPath(name, false);
            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                // 2.1 首先尝试使用AppClassLoader的父加载器进行加载
                // 主要是为了不Tomcat类目下自定义类覆盖JavaSE的核心类
                URL url = javaseLoader.getResource(resourceName);
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                tryLoadingFromJavaseLoader = true;
            }

            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
            // 将加载类的全限定名传入filter()方法判断是否须要委托给父加载器进行加载
            boolean delegateLoad = delegate || filter(name, true);

            // 2.2 若是须要进行委托加载,则交由AppClassLoader进行类的加载
            // 而AppClassLoader是遵循双亲委派模型的
            if (delegateLoad) {
                try {
                    // 交由父类AppClassLoader进行加载
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) { 
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // 2.2 不须要进行委托,则破坏双亲委派机制,首先经过WebAppClassLoader加载器加载
            // 具体的类加载方法由自身的findClass()方法进行实现
            // 这里能够看到,loadClass()方法通常用于实现类加载器模型,
            // 而由findClass()方法负责实现具体的类加载手段
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // 2.3 若是当前Web应用Class目录下不存在当前须要加载的类
            // 则从新按照双亲委派模型使用父加载器进行最终的加载尝试
            if (!delegateLoad) {
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }
        
        // 2.4 未能成功加载所须要的类,抛出ClassNotFound异常
        throw new ClassNotFoundException(name);
    }
}
复制代码

总结与参考(答误区)

  1. 类加载器的双亲委派模型经过ClassLoader::loadClass()方法经过组合父加载器实现优先父加载器加载的模型;
  2. loadClass()通常用于实现类加载器模型,诸如双亲委派模型,Tomcat类加载器模型等;findClass()则用于在定义当前类加载器模型下当前类加载器具体的加载类手段
  3. 本身实现类加载器,若是按照双亲委派模型,则只须要复写findClass()方法便可;若是须要破坏双亲委派模型,则须要复写loadClass()findClass()

主要参考:

《深刻剖析Tomcat》 《深刻理解Java虚拟机 Edition3》

你肯定你真的理解"双亲委派"了吗?!

如何本身手写一个热加载

Java类加载器 — classloader 的原理及应用

相关文章
相关标签/搜索