tomcat类加载机制

Posted 知识拾荒者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了tomcat类加载机制相关的知识,希望对你有一定的参考价值。

1 jvm类加载器

1.1 ClassLoader介绍

Java的类加载,就是把字节码格式“.class”文件加载到JVM的方法区,并在JVM的堆区建立一个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。JVM并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM类加载是由类加载器来完成的,JDK提供一个抽象类ClassLoader,这个抽象类中定义了三个关键方法,分别是loadClass、findClass、defineClass。

public abstract class ClassLoader 

    //每个类加载器都有个父加载器
    private final ClassLoader parent;
    
    public Class loadClass(String name) 
  
        //查找一下这个类是不是已经加载过了
        Class c = findLoadedClass(name);
        
        //如果没有加载过
        if( c == null )
          //先委托给父加载器去加载,注意这是个递归调用
          if (parent != null) 
              c = parent.loadClass(name);
          else 
              // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          
        
        // 如果父加载器没加载成功,调用自己的findClass去加载
        if (c == null) 
            c = findClass(name);
        
        
        return c;
    
    
    protected Class findClass(String name)
       //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
          ...
          
       //2. 调用defineClass将字节数组转成Class对象
       return defineClass(buf, off, len);
    
    
    // 将字节码数组解析成一个Class对象,用native方法实现
    protected final Class defineClass(byte[] b, int off, int len)
       ...
    
  • defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的native方法就是由C语言实现的方法,Java通过JNI机制调用。
  • findClass方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用defineClass方法得到Class对象。
  • loadClass是个public方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。

1.2 JDK类加载器

jdk有三个类加载器,另外你还可以自定义类加载器

  • BootstrapClassLoader是启动类加载器,由C语言实现,用来加载JVM启动时所需要的核心类,比如rt.jarresources.jar等。
  • ExtClassLoader是扩展类加载器,用来加载jrelibext目录下JAR包。
  • AppClassLoader是系统类加载器,用来加载classpath下的类,应用程序默认用它来加载类。
  • 自定义类加载器,用来加载自定义路径下的类。

1.3 为什么要使用双亲委托机制?

双亲委托机制是为了保证一个Java类在JVM中是唯一的,假如你不小心写了一个与JRE核心类同名的类,比如Object类,双亲委托机制能保证加载的是JRE里的那个Object类,而不是你写的Object类。这是因为AppClassLoader在加载你的Object类时,会委托给ExtClassLoader去加载,而ExtClassLoader又会委托给BootstrapClassLoader,BootstrapClassLoader发现自己已经加载过了Object类,会直接返回,不会去加载你写的Object类。 

2 Tomcat类加载器

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

2.1 findClass方法

public 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;

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

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

2.2 loadClass方法

public Class loadClass(String name, boolean resolve) throws ClassNotFoundException 

    synchronized (getClassLoadingLock(name)) 
 
        Class clazz = null;

        //1. 先在本地cache查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) 
            if (resolve)
                resolveClass(clazz);
            return clazz;
        

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

        // 3. 尝试用ExtClassLoader类加载器类加载,为什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try 
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) 
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            
         catch (ClassNotFoundException e) 
            // Ignore
        

        // 4. 尝试在本地目录搜索class并加载
        try 
            clazz = findClass(name);
            if (clazz != null) 
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            
         catch (ClassNotFoundException e) 
            // Ignore
        

        // 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
            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);

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异常。

3 Tomcat类加载器的层次结构

3.1 tomcat类加载器需要解决的问题

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

  1. 假如我们在Tomcat中运行了两个Web应用程序,两个Web应用中有同名的Servlet,但是功能不同,Tomcat需要同时加载和管理这两个同名的Servlet类,保证它们不会冲突,因此Web应用之间的类需要隔离。
  2. 假如两个Web应用都依赖同一个第三方的JAR包,比如Spring,那Spring的JAR包被加载到内存后,Tomcat要保证这两个Web应用能够共享,也就是说Spring的JAR包只被加载一次,否则随着依赖的第三方JAR包增多,JVM的内存会膨胀。
  3. 跟JVM一样,我们需要隔离Tomcat本身的类和Web应用的类。

3.2 Tomcat类加载器的层次结构

我们先来看第1个问题,假如我们使用JVM默认AppClassLoader来加载Web应用,AppClassLoader只能加载一个Servlet类,在加载第二个同名Servlet类时,AppClassLoader会返回第一个Servlet类的Class实例,这是因为在AppClassLoader看来,同名的Servlet类只被加载一次。

因此Tomcat的解决方案是自定义一个类加载器WebAppClassLoader, 并且给每个Web应用创建一个类加载器实例。我们知道,Context容器组件对应一个Web应用,因此,每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间,每一个Web应用都有自己的类空间,Web应用之间通过各自的类加载器互相隔离。

SharedClassLoader

我们再来看第2个问题,本质需求是两个Web应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享JRE的核心类。因此Tomcat的设计者又加了一个类加载器SharedClassLoader,作为WebAppClassLoader的父加载器,专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类,就会委托父加载器SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,这样共享的问题就解决了。

CatalinaClassLoader

我们来看第3个问题,如何隔离Tomcat本身的类和Web应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此Tomcat又设计一个类加载器CatalinaClassLoader,专门来加载Tomcat自身的类。这样设计有个问题,那Tomcat和各Web应用之间需要共享一些类时该怎么办呢?

CommonClassLoader

老办法,还是再增加一个CommonClassLoader,作为CatalinaClassLoader和SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader 使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

4 spring的加载问题

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

public static Class forName(String className) 
    Class caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);

可以看到在forName的函数里,会用调用者也就是Spring的加载器去加载业务类。

我在前面提到,Web应用之间共享的JAR包可以交给SharedClassLoader来加载,从而避免重复加载。Spring作为共享的第三方JAR包,它本身是由SharedClassLoader来加载的,Spring又要去加载业务类,按照前面那条规则,加载Spring的类加载器也会用来加载业务类,但是业务类在Web应用目录下,不在SharedClassLoader的加载路径下,这该怎么办呢?

于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来,用来加载Bean。Spring取线程上下文加载的代码如下:

cl = Thread.currentThread().getContextClassLoader();

在StandardContext的启动方法里,会将当前线程的上下文加载器设置为WebAppClassLoader。

originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);

在启动方法结束的时候,还会恢复线程的上下文加载器:

Thread.currentThread().setContextClassLoader(originalClassLoader);

以上是关于tomcat类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

JVM17_Tomcat打破双亲委派机制执行顺序底层代码原理Tomcat|JDBC破坏双亲委派机制带来的面试题

Tomcat打破双亲委派机制执行顺序底层代码原理JVM04_Tomcat JDBC破坏双亲委派机制带来的面试

打破双亲委派JVM:类加载机制深度剖析 - 第8篇

tomcat打破双亲委派机制

从源码理解双亲委派机制,原来如此简单

Tomcat如何打破双亲委托机制?