一 双亲委派

1 原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

2 采用双亲委托机制加载类过程

Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

    3 为什么使用该模型?

    1 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。 2 考虑到安全因素。

    二 类加载过程

    1.类加载的时机。

    image.png7个步骤,加载、验证、准备、初始化和卸载顺序是确定的,解析却不是,为了实现动态绑定。对于一个类何时被加载,jvm规范并没有强制要求,但是对于何时初始化进行了强制要求:

    1.1 主动引用时,jvm规范有且只有5种场景类才会被初始化:

  4. new getstatic putstatic 或invokestatic (被final修饰、已在编译期把结果放在常量池的静态字段除外)这四条指令。

  5. 反射调用时。
  6. 初始化一个类,其父类还未能未被初始化,先初始化父类。
  7. 虚拟机启动,用户指定执行主类(main()).
  8. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化

    1.2被动引用:

  9. 子类的静态成员来自于父类,直接调用SubClass,父类会被加载,子类则不会(与jvm具体实现有关,Hotspot采取这种方式)。

  10. 通过数组定义一个类的的数组,如new SubClass[10],SubClass本身没有被初始化。生成了一个叫[Lorg,fenixsoft.classloading.SuperClass类。有jvm生成,且初始化这个类。
  11. 直接引用一个类的常量,则不会初始化这个类。

    1.3 接口只有主动引用的第三种

    2 类加载的过程

    2.1 加载

    在加载过程。jvm必须完成3件事:

  12. 通过类的全限定名获取此类的二进制字节流

  13. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  14. 在内存中生成一个代表这个类的类对象,作为方法区这个类的各种数据的访问入口。

能从哪里获得:

  1. ZIP包jar、war、ear
  2. 网络中,如Applet
  3. 运行时动态生成;如jdk动态代理
  4. 由其他文件生成,如:jsp
  5. 数据库中获取,如SAP Netweaver

加载阶段是程序员控制能力最强也是唯一控制的阶段:如自定义加载器控制一个类加载的过程,。加载完成后,二进制字节流按jvm规定的格式存储(各个jvm格式不一定一致)在方法区中。生成在内存(不一定在堆中)中的类对象,对于hotspot虚拟机,这个对象也在方法区。

2.2 验证

class文件不应有java代码编译产生,甚至可以直接十六进制编译器直接编写class文件。4个验证动作:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

    2.3 准备阶段

    准备阶段是正式为类成员在方法区分配内存的时机,即被static修饰的成员。int为0;具体赋值在类初始化调用类构造器()方法中。被final修饰的除外。javac会将final编译为constantValue属性。

    2.4解析阶段

    解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
    符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    2.5 初始化

    类加载的最后一步,真正执行java代码的阶段,执行类的类构造器()方法。生成规则:

  5. 由编译收集,静态初始化块和类成员赋值合并产生,顺序是有定义的顺序产生。

  6. 不需要显示调用父类的方法,jvm会保证调用子类的clinit>()之前,调用父类的()
  7. 对于类和接口不是必须的
  8. 接口中,没有静态初始化块,可以有static成员,执行接口的方法不需要执行其父类的方法。只有使用父类的变量,才执行。接口的实现类一一样不会执行接口的方法。
  9. jvm会保证一个类的方法在多线程下,只有一个线程区执行方法

    3 类加载器

    每一个类加载器都有一个独立的类名称空间,比较两个类是否相等(equals() 、isAssignableFron() 、isInstance()),前提是这两个类是由统一个类加载器加载的。
    java程序员的角度有3种类加载器:

    1 启动类加载器Bootstrap ClassLoader,

    负责将/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中**。
    可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
    URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
    for (URL url : urLs) {
    System.out.println(url.toExternalForm());
    }1234
    程序执行结果如下:
    file:/C:/Java/jdk1.8.0_101/jre/lib/resources.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/rt.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/sunrsasign.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/jsse.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/jce.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/charsets.jar
    file:/C:/Java/jdk1.8.0_101/jre/lib/jfr.jar
    file:/C:/Java/jdk1.8.0_101/jre/classes12345678
    从rt.jar中选择String类,看一下String类的类加载器是什么
    ClassLoader classLoader = String.class.getClassLoader();
    System.out.println(classLoader);
    执行结果如下:
    null
    可知由于BootstrapClassLoader对Java不可见,所以返回了null,我们也可以通过某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassLoader进行加载的依据.另外上面提到ExtClassLoader的父加载器返回的是null,那是否说明ExtClassLoader的父加载器是BootstrapClassLoader?
    Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象

    2 扩展类加载器(Extensions class loader):

    该类加载器在此目录里面查找并加载 Java 类。扩展类加载器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dirs指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
    java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制).
    ExtClassLoader的类扫描路径通过执行下面代码来看一下:
    String extDirs = System.getProperty(“java.ext.dirs”);
    for (String path : extDirs.split(“;”)) {
    System.out.println(path);
    }1234
    执行结果如下:
    C:\Java\jdk1.8.0_101\jre\lib\ext
    C:\Windows\Sun\Java\lib\ext12
    其中C:\Java\jdk1.8.0_101\jre\lib\ext路径下内容为:
    3.1 类加载 - 图2
    从上面的路径中随意选择一个类,来看看他的类加载器是什么:
    ClassLoader classLoader = sun.security.ec.SunEC.class.getClassLoader();
    System.out.println(classLoader);
    System.out.println(classLoader.getParent());123
    执行结果如下:
    sun.misc.Launcher$ExtClassLoader@6bc7c054
    null
    从上面的程序运行结果可知ExtClassLoader的父加载器为null

    3 系统类加载器(System class loader)

    系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
    java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径.
    public class AppClassLoaderTest { public static void main(String[] args) { System.out.println(ClassLoader.getSystemClassLoader()); } }
    输出结果如下:
    sun.misc.Launcher$AppClassLoader@73d16e931
    以上结论说明调用ClassLoader.getSystemClassLoader()可以获得AppClassLoader类加载器.
    protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }
    通过查看ClassLoader的源码发现并且在没有特定说明的情况下,用户自定义的任何类加载器都将该类加载器作为自定义类加载器的父加载器.
    String classPath = System.getProperty(“java.class.path”); for (String path : classPath.split(“;”)) { System.out.println(path); }
    通过执行上面的代码即可获得classpath的加载路径.
    在上面的main函数的类的加载就是使用AppClassLoader加载器进行加载的,可以通过执行下面的代码得出这个结论
    public class AppClassLoaderTest { public static void main(String[] args) { ClassLoader classLoader = Test.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); } private static class Test { } }
    执行结果如下:
    sun.misc.Launcher$AppClassLoader@73d16e93 sun.misc.Launcher$ExtClassLoader@15db974212
    从上面的运行结果可以得知AppClassLoader的父加载器是ExtClassLoader
    image.png
    过程:一个类加载器收到了类加载请求,它首先不会自己加载,而是首先使用父类进行加载,最终都是委派给模型最顶层的启动类加载器,只有父类无法加载,自加载器才会尝试自己加载。
    双委派模型对java程序的正常运行和安全非常重要。**
    破坏双亲委派模型:1.双委派模型出现之前,覆盖loadClass方法。JDK 1.2 以后,吧自己的逻辑放入到findClass方法中。2.基础类调回用户代码,如JDBC、JNDI、JCE、JAXB 、JBI3.最求程序动态性产生的,HotSwap HotDeployment,如OSGI框架

    4 ContextClassLoader类加载器

    ContextClassLoader是一种与线程相关的类加载器,类似ThreadLocal,每个线程对应一个上下文类加载器.在实际使用时一般都用下面的经典结构:
    ClassLoader targetClassLoader = null;// 外部参数 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(targetClassLoader); // TODO } catch (Exception e) { e.printStackTrace(); } finally { Thread.currentThread().setContextClassLoader(contextClassLoader); }

  10. 首先获取当前线程的线程上下文类加载器并保存到方法栈,然后将外部传递的类加载器设置为当前线程上下文类加载器

  11. doSomething则可以利用新设置的类加载器做一些事情
  12. 最后在设置当前线程上下文类加载器为老的类加载器

上面的使用场景是什么?Java默认的类加载机制是委托机制,但是有些时候需要破坏这种固定的机制
具体来说,比如Java中的SPI(Service Provider Interface)是面向接口编程的,服务规则提供者会在JRE的核心API里面提供服务访问接口,而具体的实现则由其他开发商提供.我们知道Java核心API,比如rt.jar包,是使用Bootstrap ClassLoader加载的,而用户提供的jar包再有AppClassLoader加载.并且我们知道一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载.那么Bootstrap ClassLoader加载了服务提供者在rt.jar里面提供的搜索开发上提供的实现类的API类(ServiceLoader),那么这些API类里面依赖的类应该也是有Bootstrap ClassLoader来加载.而上面说了用户提供的Jar包有AppClassLoader加载,所以需要一种违反双亲委派模型的方法,线程上下文类加载器ContextClassLoader就是为了解决这个问题.


下面使用JDBC来具体说明,JDBC是基于SPI机制来发现驱动提供商提供的实现类,提供者只需在JDBC实现的jar的META-INF/services/java.sql.Driver文件里指定实现类的方式暴露驱动提供者.例如:MYSQL实现的jar如下:
3.1 类加载 - 图4
其中MYSQL的驱动如下实现了java.sql.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver1
引入MySQL驱动的jar包,测试类如下:
import java.sql.Driver; import java.util.Iterator; import java.util.ServiceLoader; public class MySQLClassLoader { public static void main(String[] args) { ServiceLoader loader = ServiceLoader.load(Driver.class); Iterator iterator = loader.iterator(); while (iterator.hasNext()) { Driver driver = (Driver) iterator.next(); System.out.println(“driver:” + driver.getClass() + “,loader:” + driver.getClass().getClassLoader()); } System.out.println(“current thread contxtloader:” + Thread.currentThread().getContextClassLoader()); System.out.println(“ServiceLoader loader:” + ServiceLoader.class.getClassLoader()); } }
执行结果如下:
driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@2a139a55 driver:class com.mysql.fabric.jdbc.FabricMySQLDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55 driver:class com.alibaba.druid.proxy.DruidDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55 driver:class com.alibaba.druid.mock.MockDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55 current thread contxtloader:sun.misc.Launcher$AppClassLoader@2a139a55 ServiceLoader loader:null123456
从执行结果中可以知道ServiceLoader的加载器为Bootstrap,因为这里输出了null,并且从该类在rt.jar里面,也可以证明.
当前线程上下文类加载器为AppClassLoader.而com.mysql.jdbc.Driver则使用AppClassLoader加载.我们知道如果一个类中引用了另外一个类,那么被引用的类也应该由引用方类加载器来加载,而现在则是引用方ServiceLoader使用BootStartClassLoader加载,被引用方则使用子加载器APPClassLoader来加载了.是不是很诡异.
下面来看一下ServiceLoader的load方法源码:
public static ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } public static ServiceLoader load(Class service) { // 获取当前线程上下文加载器,这里是APPClassLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }1234567891011
上述代码获得了线程上下文加载器(其实就是AppClassLoader),并将该类加载器传递到下面的ServiceLoader类的构造方法loader成员变量中:
private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, “Service interface cannot be null”); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); }
上述loader变量什么时候使用的?看一下下面的代码:
public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 使用loader类加载器加载 // 至于cn怎么来的,可以参照next()方法 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, “Provider “ + cn + “ not found”); } if (!service.isAssignableFrom(c)) { fail(service, “Provider “ + cn + “ not a subtype”); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, “Provider “ + cn + “ could not be instantiated”, x); } throw new Error(); // This cannot happen }
到目前为止:ContextClassLoader的作用都是为了破坏Java类加载委托机制,JDBC规范定义了一个JDBC接口,然后使用SPI机制提供的一个叫做ServiceLoader的Java核心API(rt.jar里面提供)用来扫描服务实现类,服务实现者提供的jar,比如MySQL驱动则是放到我们的classpath下面.从上文知道默认线程上下文类加载器就是AppClassLoader,所以例子里面没有显示在调用ServiceLoader前设置线程上下文类加载器为AppClassLoader,ServiceLoader内部则获取当前线程上下文类加载器(这里为AppClassLoader)来加载服务实现者的类,这里加载了classpath下的MySQL的驱动实现.
可以尝试在调用ServiceLoader的load方法前设置线程上下文类加载器为ExtClassLoader,代码如下:
Thread.currentThread().setContextClassLoader(ContextClassLoaderTest.class.getClassLoader().getParent());
然后运行本例子,设置后ServiceLoader内部则获取当前线程上下文类加载器为ExtClassLoader,然后会尝试使用ExtClassLoader去查找JDBC驱动实现,而ExtClassLoader扫描类的路径为:JAVA_HOME/jre/lib/ext/,而这下面没有驱动实现的Jar,所以不会查找到驱动.
总结下,当父类加载器需要加载子类加载器中的资源时,可以通过设置和获取线程上下文类加载器来实现.

5 Java Web服务器类加载器

Tomcat、Jetty、WebLogic、WebSphere或其他笔者没有列举的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web容器,要解决如下几个问题:
1)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
2)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
3)Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web容器自身也是使用Java语言来实现的。因此,Web容器本身也有类库依赖的问题,一般来说,基于安全考虑,容器所使用的类库应该与应用程序的类库互相独立
4)支持JSP应用的Web容器,大多数都需要支持HotSwap功能。我们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web容器都会支持JSP生成类的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。
由于存在上述问题,在部署Web应用时,单独的一个Class Path就无法满足需求了,所以各种Web容都“不约而同”地提供了好几个Class Path路径供用户存放第三方类库,这些路径一般都以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。现在,就以Tomcat容器为例,看一看Tomcat具体是如何规划用户类库结构和类加载器的。
在Tomcat目录结构中,有3组目录(“/common/”、“/server/”和“/shared/”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:
①放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。 ②放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。 ③放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。 ④放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。
3.1 类加载 - 图5
上图中灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用已经介绍过了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器
从图中的委派关系中可以看出,CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立Catalina ClassLoader和Shared ClassLoader的实例,否则在用到这两个类加载器的地方都会用Common ClassLoader的实例代替,而默认的配置文件中没有设置这两个loader项,所以Tomcat 6.x顺理成章地把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。这是Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用Tomcat 5.x的加载器架构。
Tomcat加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。如果读者阅读完上面的案例后,能完全理解Tomcat设计团队这样布置加载器架构的用意,那说明已经大致掌握了类加载器“主流”的使用方式,那么笔者不妨再提一个问题让读者思考一下:前面曾经提到过一个场景,如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?如果研究过虚拟机类加载器机制中的双亲委派模型,相信读者可以很容易地回答这个问题。
分析:如果按主流的双亲委派机制,显然无法做到让父类加载器加载的类去访问子类加载器加载的类,上面在类加载器一节中提到过通过线程上下文方式传播类加载器。
答案是使用线程上下文类加载器来实现的,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。看spring源码发现,spring加载类所用的Classloader是通过Thread.currentThread().getContextClassLoader()来获取的,而当线程创建时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终可以获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,以后任何一个线程都可以通过getContextClassLoader()获取到WebAppClassLoader来getbean了。

6 tomcat ClassLoader类加载器和WebappClassLoader类加载器

基于apache-tomcat-8.5.12.看一下Tomcat的源码Bootstrap类的initClassLoader方法,代码如下:
private void initClassLoaders() { try { // 创建commonLoader // 这里父加载器传递null,但是内部会使用默认的类加载器AppClassLoader作为父加载器. commonLoader = createClassLoader(“common”, null); if( commonLoader == null ) { // no config file, default to this loader - we might be in a ‘single’ env. commonLoader=this.getClass().getClassLoader(); } // 创建catalinaLoader,父加载器为commonLoader catalinaLoader = createClassLoader(“server”, commonLoader); // 创建sharedLoader,父类加载器为commonLoader sharedLoader = createClassLoader(“shared”, commonLoader); } catch (Throwable t) { handleThrowable(t); log.error(“Class loader creation threw exception”, t); System.exit(1); } }123456789101112131415161718192021
通过initClassLoaders方法可以得知上面三个加载器关系.下面具体看一下createClassLoader是如何构造类加载器的.代码如下:
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { // 获取catalina.properties文件中配置项分别为:common.loader/server.loader/shared.loader // 用来设置对应类加载器扫描类的路径.默认内容如下: String value = CatalinaProperties.getProperty(name + “.loader”); if ((value == null) || (value.equals(“”))) return parent; // 依据common.loader/server.loader/shared.loader的配置装饰类加载器所需的扫描路径. value = replace(value); List repositories = new ArrayList<>(); String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository try { @SuppressWarnings(“unused”) URL url = new URL(repository); repositories.add( new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith(“.jar”)) { repository = repository.substring (0, repository.length() - “.jar”.length()); repositories.add( new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(“.jar”)) { repositories.add( new Repository(repository, RepositoryType.JAR)); } else { repositories.add( new Repository(repository, RepositoryType.DIR)); } } return ClassLoaderFactory.createClassLoader(repositories, parent); }123456789101112131415161718192021222324252627282930313233343536373839404142434445 common.loader=”${catalina.base}/lib”,”${catalina.base}/lib/.jar”,”${catalina.home}/lib”,”${catalina.home}/lib/.jar” server.loader= shared.loader=123
所以依据上面的代码可知,commonLoader/serverLoader/sharedLoader是同一个ClassLoader.
具体创建类加载器的是ClassLoaderFactory.createClassLoader(repositories, parent);,具体代码如下:
public static ClassLoader createClassLoader(List repositories, final ClassLoader parent) throws Exception { if (log.isDebugEnabled()) log.debug(“Creating new class loader”); // Construct the “class path” for this class loader Set set = new LinkedHashSet<>(); if (repositories != null) { for (Repository repository : repositories) { if (repository.getType() == RepositoryType.URL) { URL url = buildClassLoaderUrl(repository.getLocation()); if (log.isDebugEnabled()) log.debug(“ Including URL “ + url); set.add(url); } else if (repository.getType() == RepositoryType.DIR) { File directory = new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.DIR)) { continue; } URL url = buildClassLoaderUrl(directory); if (log.isDebugEnabled()) log.debug(“ Including directory “ + url); set.add(url); } else if (repository.getType() == RepositoryType.JAR) { File file=new File(repository.getLocation()); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } URL url = buildClassLoaderUrl(file); if (log.isDebugEnabled()) log.debug(“ Including jar file “ + url); set.add(url); } else if (repository.getType() == RepositoryType.GLOB) { File directory=new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.GLOB)) { continue; } if (log.isDebugEnabled()) log.debug(“ Including directory glob “ + directory.getAbsolutePath()); String filenames[] = directory.list(); if (filenames == null) { continue; } for (int j = 0; j < filenames.length; j++) { String filename = filenames[j].toLowerCase(Locale.ENGLISH); if (!filename.endsWith(“.jar”)) continue; File file = new File(directory, filenames[j]); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } if (log.isDebugEnabled()) log.debug(“ Including glob jar file “ + file.getAbsolutePath()); URL url = buildClassLoaderUrl(file); set.add(url); } } } } // Construct the class loader itself final URL[] array = set.toArray(new URL[set.size()]); if (log.isDebugEnabled()) for (int i = 0; i < array.length; i++) { log.debug(“ location “ + i + “ is “ + array[i]); } // (1) return AccessController.doPrivileged( new PrivilegedAction() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
依据(1)可以创建classLoader时parent=null,这时候使用了单个参数的URLClassLoader创建了URLClassLoader类加载器作为commonLoader,而URLClassLoader默认的父加载器为AppClassLoader.那么URLClassLoader是什么呢?看一下下面的UML类图:
3.1 类加载 - 图6
到这里总结一下,默认情况下Tomcat的commonLoader/serverLoader/sharedLoader是同一个加载器,其类查找路径都是都一个地方.其实catalinaLoader主要的工作是加载Tomcat本身启动所需要的类,而sharedLoader是下文将要说的WebAppclassloader的父类,所以作用是加载所有应用都需要的类,而commonLoader做为sharedLoader/catalinaLoader的父类,自然设计目的是为了加载二者共享的类.所以如果能恰当的使用Tomcat设计的这种策略,修改catalina.properties中三种加载器类加载路径,就会真正达到这种设计效果.




在Tomcat中可以部署多个应用,每个应用使用自己独立的一个WebappClassLoader来加载应用,下面先来看一下WebappClassLoader是如何被创建的,先来看一下Tomcat的容器内部构造.
3.1 类加载 - 图7
在Tomcat中可以部署多个应用,每个应用使用自己独立的一个WebappClassLoader来加载应用,下面先来看一下WebappClassLoader是如何被创建的,先来看一下Tomcat的容器内部构造.Engine时最大的容器,默认实现为StandardEngine,里面可以有若干个Host.Host的默认实现为StandardHost,Host的父容器为Engine.每个Host容器里面有若干个Context容器,Context容器的默认实现为StandardContext,context容器的父容器为Host,每个Context代表一个应用.而创建WebappClassLoader的就是在StandardContext的startInternal方法,实现如下:
/ Start this component and implement the requirements of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. @exception LifecycleException if this component detects a fatal error that prevents this component from being used / @Override protected synchronized void startInternal() throws LifecycleException { if(log.isDebugEnabled()) log.debug(“Starting “ + getBaseName()); // Send j2ee.state.starting notification if (this.getObjectName() != null) { Notification notification = new Notification(“j2ee.state.starting”, this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification); } setConfigured(false); boolean ok = true; // Currently this is effectively a NO-OP but needs to be called to // ensure the NamingResources follows the correct lifecycle if (namingResources != null) { namingResources.start(); } // Post work directory postWorkDirectory(); // Add missing components as necessary if (getResources() == null) { // (1) Required by Loader if (log.isDebugEnabled()) log.debug(“Configuring default Resources”); try { setResources(new StandardRoot(this)); } catch (IllegalArgumentException e) { log.error(sm.getString(“standardContext.resourcesInit”), e); ok = false; } } if (ok) { resourcesStart(); } // 创建WebappLoader对象,并调用getParentClassLoader()获取到sharedLoader. if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); } // An explicit cookie processor hasn’t been specified; use the default if (cookieProcessor == null) { cookieProcessor = new Rfc6265CookieProcessor(); } // Initialize character set mapper getCharsetMapper(); // Validate required extensions boolean dependencyCheck = true; try { dependencyCheck = ExtensionValidator.validateApplication (getResources(), this); } catch (IOException ioe) { log.error(sm.getString(“standardContext.extensionValidationError”), ioe); dependencyCheck = false; } if (!dependencyCheck) { // do not make application available if dependency check fails ok = false; } // Reading the “catalina.useNaming” environment variable String useNamingProperty = System.getProperty(“catalina.useNaming”); if ((useNamingProperty != null) && (useNamingProperty.equals(“false”))) { useNaming = false; } if (ok && isUseNaming()) { if (getNamingContextListener() == null) { NamingContextListener ncl = new NamingContextListener(); ncl.setName(getNamingContextName()); ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite()); addLifecycleListener(ncl); setNamingContextListener(ncl); } } // Standard container startup if (log.isDebugEnabled()) log.debug(“Processing standard container startup”); // Binding thread ClassLoader oldCCL = bindThread(); try { if (ok) { // 调用WebappLoader的start方法创建当前应用的WebappClassLoader加载器 // Start our subordinate components, if any Loader loader = getLoader(); if (loader instanceof Lifecycle) { ((Lifecycle) loader).start(); } // since the loader just started, the webapp classloader is now // created. setClassLoaderProperty(“clearReferencesRmiTargets”, getClearReferencesRmiTargets()); setClassLoaderProperty(“clearReferencesStopThreads”, getClearReferencesStopThreads()); setClassLoaderProperty(“clearReferencesStopTimerThreads”, getClearReferencesStopTimerThreads()); setClassLoaderProperty(“clearReferencesHttpClientKeepAliveThread”, getClearReferencesHttpClientKeepAliveThread()); // By calling unbindThread and bindThread in a row, we setup the // current Thread CCL to be the webapp classloader unbindThread(oldCCL); oldCCL = bindThread(); // Initialize logger again. Other components might have used it // too early, so it should be reset. logger = null; getLogger(); Realm realm = getRealmInternal(); if(null != realm) { if (realm instanceof Lifecycle) { ((Lifecycle) realm).start(); } // Place the CredentialHandler into the ServletContext so // applications can have access to it. Wrap it in a “safe” // handler so application’s can’t modify it. CredentialHandler safeHandler = new CredentialHandler() { @Override public boolean matches(String inputCredentials, String storedCredentials) { return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials); } @Override public String mutate(String inputCredentials) { return getRealmInternal().getCredentialHandler().mutate(inputCredentials); } }; context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler); } // Notify our interested LifecycleListeners fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null); // Start our child containers, if not already started for (Container child : findChildren()) { if (!child.getState().isAvailable()) { child.start(); } } // Start the Valves in our pipeline (including the basic), // if any if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); } // Acquire clustered manager Manager contextManager = null; Manager manager = getManager(); if (manager == null) { if (log.isDebugEnabled()) { log.debug(sm.getString(“standardContext.cluster.noManager”, Boolean.valueOf((getCluster() != null)), Boolean.valueOf(distributable))); } if ( (getCluster() != null) && distributable) { try { contextManager = getCluster().createManager(getName()); } catch (Exception ex) { log.error(“standardContext.clusterFail”, ex); ok = false; } } else { contextManager = new StandardManager(); } } // Configure default manager if none was specified if (contextManager != null) { if (log.isDebugEnabled()) { log.debug(sm.getString(“standardContext.manager”, contextManager.getClass().getName())); } setManager(contextManager); } if (manager!=null && (getCluster() != null) && distributable) { //let the cluster know that there is a context that is distributable //and that it has its own manager getCluster().registerManager(manager); } } if (!getConfigured()) { log.error(sm.getString(“standardContext.configurationFail”)); ok = false; } // We put the resources into the servlet context if (ok) getServletContext().setAttribute (Globals.RESOURCES_ATTR, getResources()); if (ok ) { if (getInstanceManager() == null) { javax.naming.Context context = null; if (isUseNaming() && getNamingContextListener() != null) { context = getNamingContextListener().getEnvContext(); } Map> injectionMap = buildInjectionMap( getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources()); setInstanceManager(new DefaultInstanceManager(context, injectionMap, this, this.getClass().getClassLoader())); } getServletContext().setAttribute( InstanceManager.class.getName(), getInstanceManager()); InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager()); } // Create context attributes that will be required if (ok) { getServletContext().setAttribute( JarScanner.class.getName(), getJarScanner()); } // Set up the context init params mergeParameters(); // Call ServletContainerInitializers for (Map.Entry>> entry : initializers.entrySet()) { try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString(“standardContext.sciFail”), e); ok = false; break; } } // Configure and call application event listeners if (ok) { if (!listenerStart()) { log.error(sm.getString(“standardContext.listenerFail”)); ok = false; } } // Check constraints for uncovered HTTP methods // Needs to be after SCIs and listeners as they may programmatically // change constraints if (ok) { checkConstraintsForUncoveredMethods(findConstraints()); } try { // Start manager Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); } } catch(Exception e) { log.error(sm.getString(“standardContext.managerFail”), e); ok = false; } // Configure and call application filters if (ok) { if (!filterStart()) { log.error(sm.getString(“standardContext.filterFail”)); ok = false; } } // Load and initialize all “load on startup” servlets if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString(“standardContext.servletFail”)); ok = false; } } // Start ContainerBackgroundProcessor thread super.threadStart(); } finally { // Unbinding thread unbindThread(oldCCL); } // Set available status depending upon startup success if (ok) { if (log.isDebugEnabled()) log.debug(“Starting completed”); } else { log.error(sm.getString(“standardContext.startFailed”, getName())); } startTime=System.currentTimeMillis(); // Send j2ee.state.running notification if (ok && (this.getObjectName() != null)) { Notification notification = new Notification(“j2ee.state.running”, this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification); } // The WebResources implementation caches references to JAR files. On // some platforms these references may lock the JAR files. Since web // application start is likely to have read from lots of JARs, trigger // a clean-up now. getResources().gc(); // Reinitializing if something went wrong if (!ok) { setState(LifecycleState.FAILED); } else { setState(LifecycleState.STARTING); } }
进入WebappLoader的startInternal方法如下:
@Override protected void startInternal() throws LifecycleException { if (log.isDebugEnabled()) log.debug(sm.getString(“webappLoader.starting”)); if (context.getResources() == null) { log.info(“No resources for “ + context); setState(LifecycleState.STARTING); return; } // Construct a class loader based on our current repositories list try { classLoader = createClassLoader(); classLoader.setResources(context.getResources()); classLoader.setDelegate(this.delegate); // Configure our repositories setClassPath(); setPermissions(); ((Lifecycle) classLoader).start(); String contextName = context.getName(); if (!contextName.startsWith(“/“)) { contextName = “/“ + contextName; } ObjectName cloname = new ObjectName(context.getDomain() + “:type=” + classLoader.getClass().getSimpleName() + “,host=” + context.getParent().getName() + “,context=” + contextName); Registry.getRegistry(null, null) .registerComponent(classLoader, cloname, null); } catch (Throwable t) { t = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(t); log.error( “LifecycleException “, t ); throw new LifecycleException(“start: “, t); } setState(LifecycleState.STARTING); }
其中startInternal方法中createClassLoader具体如下:
/
Create associated classLoader. / private WebappClassLoaderBase createClassLoader() throws Exception { Class<?> clazz = Class.forName(loaderClass); WebappClassLoaderBase classLoader = null; if (parentClassLoader == null) { parentClassLoader = context.getParentClassLoader(); } Class<?>[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor<?> constr = clazz.getConstructor(argTypes); classLoader = (WebappClassLoaderBase) constr.newInstance(args); return classLoader; }
上述代码创建并实例化一个webappClassLoader,并设置父类加载器为sharedLoader.
到现在为止创建了应用的类加载器,由于每个standardcontext对应一个Web应用,所以不同的应用都有不同的WebappClassLoader,共同点是他们的父加载器都是sharedLoader.下面是Tomcat的类加载器关系图:
3.1 类加载 - 图8




看一下WebappClassLoaderBase中的loadClass方法实现:
@Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { if (log.isDebugEnabled()) log.debug(“loadClass(“ + name + “, “ + resolve + “)”); Class<?> clazz = null; // Log access to stopped class loader checkStateForClassLoading(name); // 首先检查WebappClassLoader缓存中是否已经加载该类 // (0) Check our previously loaded local class cache clazz = findLoadedClass0(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(“ Returning class from cache”); if (resolve) resolveClass(clazz); return (clazz); } // 看jvm缓存是否已经加载该类 // (0.1) Check our previously loaded class cache clazz = findLoadedClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(“ Returning class from cache”); if (resolve) resolveClass(clazz); return (clazz); } // 为了避免webapp覆盖Java SE classes,这里尝试使用ExtClassLoader进行加载. // Tomcat8对这个做了优化,之前是直接调用loadClass方法进行尝试加载,如果不存在则抛出ClassNotFoundException异常 // 这个异常虽然会被catch调用,但是抛出ClassNotFounrException异常的代码非常高,所以Tomcat8做了个优化,就是先调用 // getResource判断路径下是否存在(getResource的调用不会产生昂贵的代价),存在才会调用loadClass进行加载. String resourceName = binaryNameToPath(name, false); ClassLoader javaseLoader = getJavaseClassLoader(); boolean tryLoadingFromJavaseLoader; try { tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); tryLoadingFromJavaseLoader = true; } if (tryLoadingFromJavaseLoader) { try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } // (0.5) Permission to access this class when using a SecurityManager if (securityManager != null) { int i = name.lastIndexOf(‘.’); if (i >= 0) { try { securityManager.checkPackageAccess(name.substring(0,i)); } catch (SecurityException se) { String error = “Security Violation, attempt to use “ + “Restricted Class: “ + name; log.info(error, se); throw new ClassNotFoundException(error, se); } } } // 如果设置了委托机制则委托给父类加载进行加载. boolean delegateLoad = delegate || filter(name, true); // (1) Delegate to our parent if requested if (delegateLoad) { if (log.isDebugEnabled()) log.debug(“ Delegating to parent classloader1 “ + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(“ Loading class from parent”); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } // 调用findClass在Web应用的lib目录下进行查找 // (2) Search local repositories webapp本地搜索 if (log.isDebugEnabled()) log.debug(“ Searching local repositories”); try { clazz = findClass(name); if (clazz != null) { if (log.isDebugEnabled()) log.debug(“ Loading class from local repository”); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } // 如果上面没有设置委托则这时候再让父加载器进行加载,这个时候也是违背类加载器委托模型的一个例子. // (3) Delegate to parent unconditionally if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(“ Delegating to parent classloader at end: “ + parent); try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(“ Loading class from parent”); if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } } throw new ClassNotFoundException(name); }

7 自定义类加载器实现模块隔离

https://blog.csdn.net/sweatOtt/article/details/88996315

解决方案

归纳了解了几种业内的解决方案如下,各有优劣

  1. spring boot方式,统一管理各个组件版本,简洁高效,但遇到必须使用不同版本jar包时,就不行了
  2. OSGI技术,用容器对jar包进行暴露和隔离,实际上是通过不同classload加载类来达到目的,真正的jar包隔离,还能做模块化,服务热部署,热更新等,缺点就是太重了。
  3. sofa-ark 用FatJar技术去实现OSGI的功能,jar包隔离原理上跟osgi一致,不过基于fat jar技术,通过maven 插件来简化复杂度,比较轻量,也支持服务热部署热更新等功能
  4. shade 也有maven插件,通过更改jar包的字节码来避免jai包冲突,jar包冲突的本质是类的全限定名(包名+类名)冲突了,通过全限定名不能定位到你想用的那个类,maven-shade插件可以更改jar包里的包名,来达到解决冲突的目的。
  5. 自己定义classload,反射调用冲突方法,代码量太大,不通用,但是会帮助理解上面组件的原理。

此外,还有一些其他解决的方式,比如java 9 模块化,提供了一种新的打包方式来解决类冲突、gradle 组件、代码内嵌等。

8 Thread 类加载器何时定义的

contextClassLoader在init是父加载器透传的。