常见类加载器
BootstrapClassLoader
最底层的 启动类加载器,负责加载
下面的核心类库或-Xbootclasspath
选项指定的jar包。由native方法实现加载过程,程序无法直接获取到该类加载器,无法对其进行任何操作。
ExtClassLoader
扩展类加载器,由sun.misc.Launcher.ExtClassLoader
实现的。负责加载
或者由系统变量-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文件。
AppClassLoader
和ExtClassLoader
都是其子类,区别就是传入的URL不一样。
ThreadContextClassLoader
线程上下文类加载器,这并不是一个类,而是存储在Thread对象里的一个classsLoader对象,Thread再被创建出来的时候会从当前Thread中继承。
Launcher
在加载的时候,会通过静态内部类初始化单例的ExtClassLoader
和 AppClassLoader
,并将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
类加载器父子关系
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;
}
}
双亲委派的优点
- 双亲委派最终会保证同一个限定名的类只会被加载一次。
- 处于安全考虑, 双亲委派优先加载底层的类,这样规避了有坏人注入一个篡改后的底层类比如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自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示
从图中的委派关系中可以看出,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来加载类。
总结
- 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
- 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
关于双气委派被破坏
双亲委派正常情况下是APP主动找类,先让EXT找,EXT又先让BOOT找,上面都找不到APP再自己找。现在这种情况却是BOOT主动找类,我们清楚BOOT可定是找不到的,如果此时BOOT找不到就说我找不到了,那是符合双亲委派模型的;但是,为了还能找到,BOOT又把这个事情给TCCL(APP)了,任务到了TCCL(APP)这里,TCCL(APP)本身其实是可以找到的,然而TCCL(APP)本身未破坏双亲委派模型,所以又按照正常的流程抛了一遍,最后还得是自己找到。我猜是这里给了你一种未破坏双亲委派模型的假象。我理解的关键点是要搞清楚谁发起加载类这个事情的,然后又是如何终结的,然后再看是否符合双亲委派模型。