Java中的类加载器

常见类加载器

BootstrapClassLoader

最底层的 启动类加载器,负责加载/lib下面的核心类库或-Xbootclasspath选项指定的jar包。由native方法实现加载过程,程序无法直接获取到该类加载器,无法对其进行任何操作。

ExtClassLoader

扩展类加载器,由sun.misc.Launcher.ExtClassLoader实现的。负责加载/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库。程序可以访问并使用扩展类加载器。 父加载器是BootstrapClassLoader。

AppClassLoader

系统类加载器,由sun.misc.Launcher.AppClassLoader实现的,也叫应用程序类加载器。负责加载系统类路径-classpath-Djava.class.path变量所指的目录下的类库。程序可以访问并使用系统类加载器。父加载器是ExtClassLoader。

URLClassLoader

用于从引用 JAR 文件和目录的 URL 的搜索路径加载类和资源。任何以 '' 结尾的 URL 都被假定为指向一个目录。否则,该 URL 被假定为引用将根据需要打开的 JAR 文件。

URLClassLoader实现了findClass()方法,从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。如果不是以该字符结束,则认为该URL指向一个JAR文件。

AppClassLoaderExtClassLoader都是其子类,区别就是传入的URL不一样。

ThreadContextClassLoader

线程上下文类加载器,这并不是一个类,而是存储在Thread对象里的一个classsLoader对象,Thread再被创建出来的时候会从当前Thread中继承。
Launcher在加载的时候,会通过静态内部类初始化单例的ExtClassLoaderAppClassLoader,并将AppClassLoader设置为当前Thread的ContextClassLoader。所以ThreadContextClassLoader其实就是AppClassLoader。
ThreadContextClassLoader打破了双亲委派机制。

先来看看什么是SPI机制,引用一段博文中的介绍:

SPI机制简介
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

这里可以参考:https://blog.csdn.net/yangcheng33/article/details/52631940

类加载器父子关系

image.png

ClassLoader是类加载器的顶级抽象类,定义了loadClass()的逻辑,内部定义了一个parent属性,存放父级类加载器;除了BootstrapClassLoader之外,所有的类加载器都是ClassLoader的子类,所以这些子类都拥有一个parent属性。

双亲委派机制

所谓双亲委派机制,就是加载一个类的时候,当前类加载器会先从自己已经加载的类中去查找,如果没找到会优先调用父级加载器的loadClass()去加载,这是一个递归的过程,一定最终会走到BootstrapClassLoader,如果父级加载器中没有,会调用findClass()接口尝试从路径中加载。
这是一个向上查找,向下加载的的过程。

   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. 双亲委派最终会保证同一个限定名的类只会被加载一次。
  2. 处于安全考虑, 双亲委派优先加载底层的类,这样规避了有坏人注入一个篡改后的底层类比如String.class,因为String.class永远只会加载java提供的那个。

拓展

Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
    为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示


    image.png

    从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

Spring如何加载

Spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean

SpringBoot中Spring如何加载

SpringBoot启动的Spring使用的是TCCL AppClassLoader来加载类。

总结

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

关于双气委派被破坏

双亲委派正常情况下是APP主动找类,先让EXT找,EXT又先让BOOT找,上面都找不到APP再自己找。现在这种情况却是BOOT主动找类,我们清楚BOOT可定是找不到的,如果此时BOOT找不到就说我找不到了,那是符合双亲委派模型的;但是,为了还能找到,BOOT又把这个事情给TCCL(APP)了,任务到了TCCL(APP)这里,TCCL(APP)本身其实是可以找到的,然而TCCL(APP)本身未破坏双亲委派模型,所以又按照正常的流程抛了一遍,最后还得是自己找到。我猜是这里给了你一种未破坏双亲委派模型的假象。我理解的关键点是要搞清楚谁发起加载类这个事情的,然后又是如何终结的,然后再看是否符合双亲委派模型。

你可能感兴趣的:(Java中的类加载器)