类加载器与双亲委派机制

2021-08-05  本文已影响0人  新生代民工代表

前言

什么是类加载器?
类加载器有哪些?
双亲委派机制是怎么样的?
什么时候需要打破双亲委派?
如何打破?

以上内容网上有大量文章介绍一些基础概念,这篇《类加载器、双亲委派机制与打破(Driver、TCCL、Tomcat等)》《我竟然被“双亲委派”给虐了》有详细介绍,本文着重从源码来讲解下我在理解过程中的疑问

用途

先在前面说,了解这个有啥用,我认为有两点:

疑问

1.jvm默认的类加载器:AppClassLoader、ExtClassLoader、Bootstrap ClassLoader;三者是如何在代码里面没有继承关系,是如何进行逐步委托加载的?

首先他们三不是用extends进行继承操作的,是基于组合进行的松耦合继承,可以看ClassLoader类里面的parent属性,其次rt.jar中sun.misc.Launcher类中有两个静态类AppClassLoaderExtClassLoader,查看Launcher的构造函数如下:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //获取ext类加载器,点进getExtClassLoader()方法可以看到加载的范围为
            // String var0 = System.getProperty("java.ext.dirs");
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //获取app类加载器,点进getAppClassLoader()方法可以看到final String var1 = System.getProperty("java.class.path");
            //注意此处传入了var1,创建app时,将ext传入,跟进代码可以到顶层抽象类ClassLoader.java,代码见下文
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //注意这个线程上下文类加载器,此加载器可以用户JDBC、spring等打破双亲加载机制,默认的线程上下文加载器 == app加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
 private ClassLoader(Void unused, ClassLoader parent) {
        //此时this指向app,parent指向ext,且parent为该类的成员变量,这种关系没有使用继承,采用的基于松耦合的组合关系
       // 由于启动类加载器是c++实现的,在java的视角里是没有此对象的,所以为null;换而言之,如果this指向是ext,那么他的parent为null,此处在loadClass方法中有体现,见下文代码
        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;
        }
    }
  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
           //看看这个findLoadedClass方法的位置,说明相同的类只会加载一次
           //这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //父加载器不为空,就一直往上递归,直至交给顶层父类加载器加载,结合上面代码注释可知,从自定义加载器开始,到app、到ext,都会递归往上找parent,直到parent == null
                    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
                    //进行递归退出的处理
                }
                //从顶层开始,每一层类加载器加载不到,就会逐步往下找可以加载的类加载器,所以自定义加载器是需要重写这个findClass方法的
                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;
        }
    }

2.双亲委派模型中的"双亲"如何理解?

并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器;

  • 父类加载器(parent classloader):它可以替子加载器尝试加载类;
    他是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的,他的parent为null,由上文loadclass代码可知,为空就会调用findBootstrapClassOrNull()
  • 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。

3.为什么jdbc驱动加载要打破双亲委派

因为Driver接口是java.sql包下的,根据规定,这个应该是bootstrap classloader来加载,但是具体的实现是各个厂商来做的,bootstrap classloader无法加载实现类,这个时候就需要子类加载器加载(加载过程是逐步往上递归,父类加载不到逐步由子类来加载,由此时直接交给app来加载不就行了么,返回为null,直接走app classloader 的findclass方法进行加载不就可以么)

针对上面个问题,重新捋下逻辑
1.jdbc的加载是由DriverManager来执行,这个类在rt下面,由根加载器来加载
2.该类有个静态代码块,里面有loadInitialDrivers()方法,然后跟进方法里面可以看到 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//这儿涉及spi机制来加载厂商的实现
3.厂商的代码肯定不在rt包里面,目前的加载器是根加载器,无法加载厂商实现,jdk的方案是代码如下:

public static <S> ServiceLoader<S> load(Class<S> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
       return ServiceLoader.load(service, cl);
   }

可以看到是在当前根加载器执行加载期间,从当前线程获取到上线文加载器,去加载厂商的实现类

我的问题来了:这儿如果不用上下文来加载,直接返回加载不到,return null; 然后直接走app classloader 的findclass方法进行加载不就可以么?
答:被这个问题困扰了几天,想不通,后面猜测如果返回为空,那之前根加载到的DriverManager怎么办,是直接放弃了么,我理解的是整个DriverManager加载和厂商驱动加载是一体的原子性的,必须要在根加载器加载到一半的时候继续去加载厂商的,如果这个时候返回为空,那么后面app是可以加载厂商实现,但是DriverManager怎么办,不加载了么,要加载的话,又是根加载器来加载

这个理解不知道对不对

4.为什么tomcat要打破双亲委派模型的?

从问题1我们知道,类的加载是逐步委托给顶层的类加载器进行加载,加载不到时才会由下面的加载器进行加载;
假设我们tomcat容器中有三个server,server1的jackson是1.0;server2的jackson是2.0;server3的jackson是3.0;
三个版本里面的某个类的方法里面的实现可能不一样,但是由于双亲委派机制的存在,都会交由顶层类加载器来加载,如果没有自定义,那么应该是app加载,就会导致该类最终会被覆盖成某个版本;(类在jvm中的唯一性由类加载器名和类全路径决定)
因为我们的三个server是不同的应用,所以我们需要各个server进行隔离,独自加载,不应该使用双亲委派交由顶层加载

  1. tomcat是如何打破双亲委派模型的?
    首先推荐一篇优秀的文章 [Tomcat 的架构有哪些牛逼之处?](https://mp.weixin.qq.com/s/_bsAOTA10fGDJsz2jCYivg) 从架构、实现等多个维度层面进行分析,文章较难,需要多次阅读

org.apache.catalina.loader.WebappClassLoader,该类重写了findclass方法

ublic Class<?> findClass(String name) throws ClassNotFoundException {
    ...

    Class<?> clazz = null;
    try {
            //1. 先在 Web 应用目录下查找类
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
       }

    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
       }

    //3. 如果父类也没找到,抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }

    return clazz;
}

同样的也重写了loadclass方法

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {

        Class<?> clazz = null;

        //1. 先在本地 cache 查找该类是否已经加载过(主要是tomcat自定义加载器中查找)
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 从系统类加载器的 cache 中查找是否加载过(从jvm加载的类里面查找)
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // 3. 尝试用 ExtClassLoader 类加载器类加载(此处是精髓,注意是ext,不是app,基础类和扩展类都是交由双亲来加载,避免了覆盖 JRE 核心类,保证虚拟机的正常运行)
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 尝试在本地目录搜索 class 并加载(这儿就是tomcat自定义加载器加载了)
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
       (其他类还有交由app加载)
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }

    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
}
tomcat加载类关系图.png

6.类加载成功后会立即调用构造函数进行实例化么

不会,如果涉及到new等创建对象才会实例化,类加载后是会初始化静态代码块,静态变量等,类的初始化和对象的初始化是两个事情

7.自定义类加载器是如何保障加载器执行顺序的,即app怎么成为自定义加载器的parent

ClassLoader类里面默认的parent是app

protected ClassLoader() {
       this(checkCreateClassLoader(), getSystemClassLoader());
}

8.当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式

总结:

上一篇下一篇

猜你喜欢

热点阅读