Tomcat类加载机制

728 阅读7分钟

Tomcat类加载机制

JVM类加载器

Java中有 3 个类加载器,另外你也可以自定义类加载器

  • BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jar。
  • ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。扩展加载器的 #getParent() 方法返回 null ,但实际上扩展类加载器的父类加载器就是启动类加载器。
  • AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。程序可以通过 #getSystemClassLoader() 来获取系统类加载器。
  • 自定义类加载器,用来加载自定义路径下的类。
public class ClassLoaderDemo {

   public static void main(String[] args) {
      // BootstrapClassLoader
      System.out.println(ReentrantLock.class.getClassLoader());
      // ExtClassLoader
      System.out.println(ZipInfo.class.getClassLoader());
      // AppClassLoader
      System.out.println(ClassLoaderDemo.class.getClassLoader());

      // AppClassLoader
      System.out.println(ClassLoader.getSystemClassLoader());
      // ExtClassLoader
      System.out.println(ClassLoader.getSystemClassLoader().getParent());
      // BootstrapClassLoader
      System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

   }
}

双亲委派机制

加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。这就是双亲委派机制。

clipboard (5).png

ClassLoader#loadClass加载过程

clipboard (6).png

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

Tomcat 的类加载机制

Tomcat 作为 Servlet 容器,它负责加载我们的 Servlet 类,此外它还负责加载 Servlet 所依赖的 JAR 包。并且 Tomcat 本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。 

Tomcat是如何隔离Web应用的?

Tomcat 自定义了一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。其实现的原理就是不同的类加载器实例加载的类被认为是不同的类,即使它们的类名相同(不同类加载器实例加载的类是互相隔离的)。

Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。

在 findClass 方法里,主要有三个步骤:

  1. 先在 Web 应用本地目录下查找要加载的类。
  2. 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
  3. 如何父加载器也没找到这个类,抛出 ClassNotFound 异常。

loadClass 方法稍微复杂一点,主要有六个步骤:

  1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
  2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
  3. 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
  4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
  5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
  6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。

Tomcat类加载器的层次结构

Tomcat 拥有不同的自定义类加载器,以实现对各种资源库的控制。 Tomcat 主要用类加载器解决以下 4 个问题:

  • 同一个 Web 服务器里,各个 Web 项目之间各自使用的 Java 类库要互相隔离。
  • 同一个 Web 服务器里,各个 Web 项目之间可以提供共享的 Java 类库 。
  • 为了使服务器不受 Web 项目的影响,应该使服务器的类库与应用程序的类库互相独立。
  • 对于支持 JSP 的 Web 服务器,应该支持热插拔( HotSwap )功能 。

Tomcat提供了四组目录供用户存放第三方类库:

  • 放置在/common目录中:类库可被Tomcat和所有的 Web应用程序共同使用。
  • 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对 Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对 Tomcat和其他Web应用程序都不可见。

Tomcat自定义了多个类加载器,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

  • CommonClassLoader : Tomcat 通用类加载器, 加载的资源可被 Tomcat 和 所有的 Web 应用程序共同获取
  • CatalinaClassLoader : Tomcat 类加载器, 加载的资源只能被 Tomcat 获取(但所有 WebappClassLoader 不能获取到 catalinaLoader 加载的类)
  • SharedClassLoader : Tomcat 各个Context的父加载器, 这个类是所有 WebappClassLoader 的父类, sharedLoader 所加载的类将被所有的 WebappClassLoader 共享获取
  • WebappClassLoader : 每个Context 对应一个 WebappClassloader, 主要用于加载 WEB-INF/lib 与 WEB-INF/classes 下面的资源

clipboard (7).png

线程上下文加载器

在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。

如果spring作为共享第三方jar包,交给SharedClassLoader加载,但是业务类在web目录下,不在SharedClassLoader的加载路径下,那spring如何加载web应用目录下的业务bean呢?

 Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。

线程上下文加载器是一种类加载器传递机制,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。

Thread.currentThread().getContextClassLoader()

线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的