Tomcat类加载器机制详解

1、JVM的类加载器

1)、类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况

2)、什么是双亲委派模型

在这里插入图片描述

  • 启动类加载器(Bootstrap ClassLoader):负责将放在<JAVA HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
  • 扩展类加载器(ExtClassLoader):由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(AppClassLoader):由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载。它负责加载用户类路径(ClassPath)上所有指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

类加载之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类

3)、java.lang.ClassLoader

JDK提供一个抽象类java.lang.ClassLoader,核心方法源码如下:

public abstract class ClassLoader {
    
    
  	//每个类加载器都有个父加载器
    private final ClassLoader parent;  
  
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    
    
        synchronized (getClassLoadingLock(name)) {
    
    
            //查找一下这个类是不是已经加载过了
            Class<?> c = findLoadedClass(name);
          	//如果没有加载过
            if (c == null) {
    
    
                long t0 = System.nanoTime();
                try {
    
    
                    if (parent != null) {
    
    
                      	//先委托给父加载器去加载,注意这是个递归调用
                        c = parent.loadClass(name, false);
                    } else {
    
    
                      	//如果父加载器为空,查找Bootstrap加载器是不是加载过了
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
    
    
                }
				
                if (c == null) {
    
    
                    long t1 = System.nanoTime();
                  	//如果父加载器没加载成功,调用自己的findClass去加载
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
    
    
                resolveClass(c);
            }
            return c;
        }
    }
  
  	//根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读到内存得到字节码数组,最终将字节数组转成Class对象
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        throw new ClassNotFoundException(name);
    }  
  • 每个类加载器都持有一个parent字段,指向父加载器。类加载器的父子关系不是通过继承来实现的,比如AppClassLoader并不是ExtClassLoader的子类,而是说AppClassLoader的parent成员变量指向ExtClassLoader对象
  • findClass()方法主要职责就是找到.class文件读到内存得到字节码数组,最终将字节数组转成Class对象
  • loadClass()方法是实现双亲委派模型的关键:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载

2、Tomcat如何打破双亲委派模型

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

findClass()方法源码如下

public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
    
    
  
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
    
    

        if (log.isDebugEnabled())
            log.debug("    findClass(" + name + ")");

        checkStateForClassLoading(name);

        if (securityManager != null) {
    
    
            int i = name.lastIndexOf('.');
            if (i >= 0) {
    
    
                try {
    
    
                    if (log.isTraceEnabled())
                        log.trace("      securityManager.checkPackageDefinition");
                    securityManager.checkPackageDefinition(name.substring(0,i));
                } catch (Exception se) {
    
    
                    if (log.isTraceEnabled())
                        log.trace("      -->Exception-->ClassNotFoundException", se);
                    throw new ClassNotFoundException(name, se);
                }
            }
        }

        Class<?> clazz = null;
        try {
    
    
            if (log.isTraceEnabled())
                log.trace("      findClassInternal(" + name + ")");
            try {
    
    
                if (securityManager != null) {
    
    
                    PrivilegedAction<Class<?>> dp =
                        new PrivilegedFindClassByName(name);
                    clazz = AccessController.doPrivileged(dp);
                } else {
    
    
                    //1.先在Web应用目录下查找类
                    clazz = findClassInternal(name);
                }
            } catch(AccessControlException ace) {
    
    
                log.warn(sm.getString("webappClassLoader.securityException", name,
                        ace.getMessage()), ace);
                throw new ClassNotFoundException(name, ace);
            } catch (RuntimeException e) {
    
    
                if (log.isTraceEnabled())
                    log.trace("      -->RuntimeException Rethrown", e);
                throw e;
            }
            if ((clazz == null) && hasExternalRepositories) {
    
    
                try {
    
    
                    //2.如果在本地目录没有找到,交给父加载器去查找
                    clazz = super.findClass(name);
                } catch(AccessControlException ace) {
    
    
                    log.warn(sm.getString("webappClassLoader.securityException", name,
                            ace.getMessage()), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
    
    
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            //3.如果父类也没找到,抛出ClassNotFoundException
            if (clazz == null) {
    
    
                if (log.isDebugEnabled())
                    log.debug("    --> Returning ClassNotFoundException");
                throw new ClassNotFoundException(name);
            }
        } catch (ClassNotFoundException e) {
    
    
            if (log.isTraceEnabled())
                log.trace("    --> Passing on ClassNotFoundException");
            throw e;
        }

        if (log.isTraceEnabled())
            log.debug("      Returning class " + clazz);

        if (log.isTraceEnabled()) {
    
    
            ClassLoader cl;
            if (Globals.IS_SECURITY_ENABLED){
    
    
                cl = AccessController.doPrivileged(
                    new PrivilegedGetClassLoader(clazz));
            } else {
    
    
                cl = clazz.getClassLoader();
            }
            log.debug("      Loaded by " + cl.toString());
        }
        return clazz;

    }  

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

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

loadClass()方法源码如下

public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
    
    
  
		@Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    

        synchronized (getClassLoadingLock(name)) {
    
    
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            checkStateForClassLoading(name);

            //1.先在当前ClassLoader的本地cache中查找该类是否已经加载过
            clazz = findLoadedClass0(name);
            if (clazz != null) {
    
    
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }

            //2.调用ClassLoader的findLoadedClass方法查看JVM是否已经加载过此类
            clazz = findLoadedClass(name);
            if (clazz != null) {
    
    
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }

            //3.通过启动类加载器(Bootstrap ClassLoader)加载此类,这里防止应用写的类覆盖了JavaSE的类
            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
    
    
                URL url;
                if (securityManager != null) {
    
    
                    PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                    url = AccessController.doPrivileged(dp);
                } else {
    
    
                    url = javaseLoader.getResource(resourceName);
                }
                tryLoadingFromJavaseLoader = (url != 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) {
    
    
                }
            }

            if (securityManager != null) {
    
    
                int i = name.lastIndexOf('.');
                if (i >= 0) {
    
    
                    try {
    
    
                        securityManager.checkPackageAccess(name.substring(0,i));
                    } catch (SecurityException se) {
    
    
                        String error = sm.getString("webappClassLoader.restrictedPackage", name);
                        log.info(error, se);
                        throw new ClassNotFoundException(error, se);
                    }
                }
            }

            //4.判断是否需要委托给父类加载器进行加载
            boolean delegateLoad = delegate || filter(name, true);

            //5.因为delegatedLoad为false,那么此时不会委托父加载器去加载
            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) {
    
    
                }
            }

            //6.尝试在本地目录搜索class并加载
            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) {
    
    
            }

            //7.通过父类加载器去加载
            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) {
    
    
                }
            }
        }

        //上述过程都加载失败,抛出异常
        throw new ClassNotFoundException(name);
    }

loadClass()方法里,主要有六个步骤:

  1. 先在当前ClassLoader的本地cache中查找该类是否已经加载过
  2. 调用ClassLoader的findLoadedClass()方法查看JVM是否已经加载过此类
  3. 通过启动类加载器(Bootstrap ClassLoader)加载此类,目的是防止Web应用自己的类覆盖JRE的核心类。因为Tomcat需要打破双亲委派模型,假如Web应用里自定义了一个叫Object的类,如果先加载这个Object类,就会覆盖JRE里面的那个Object类,这就是为什么Tomcat的类加载器会优先尝试用Bootstrap ClassLoader去加载,因为Bootstrap ClassLoader发现自己已经加载了Object类,直接返回给Tomcat的类加载器,这样Tomcat的类加载器就不会去加载Web应用下的Object类了,也就避免了覆盖JRE核心类的问题
  4. 如果Bootstrap ClassLoader加载器加载失败,也就是说JRE核心类中没有这类,那么就在本地Web应用目录下查找并加载
  5. 如果本地目录下没有这个类,说明不是Web应用自己定义的类,那么由应用程序类加载器(AppClassLoader)去加载。Web应用是通过Class.forName调用交给AppClassLoader的,因为Class.forName的默认加载器就是AppClassLoader
  6. 如果上述加载过程全部失败,抛出ClassNotFound异常

这里第三步是通过启动类加载器(Bootstrap ClassLoader)加载此类,也有的文章说是通过扩展类加载器(ExtClassLoader),Tomcat 8.5x版本给javaseClassLoader赋值代码如下:

public abstract class WebappClassLoaderBase extends URLClassLoader
       implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
     
     
 
   protected WebappClassLoaderBase() {
     
     

       super(new URL[0]);

       ClassLoader p = getParent();
       if (p == null) {
     
     
           p = getSystemClassLoader();
       }
       this.parent = p;

       ClassLoader j = String.class.getClassLoader();
       if (j == null) {
     
     
           j = getSystemClassLoader();
           while (j.getParent() != null) {
     
     
               j = j.getParent();
           }
       }
       this.javaseClassLoader = j;

       securityManager = System.getSecurityManager();
       if (securityManager != null) {
     
     
           refreshPolicy();
       }
   }

可以看到javaseClassLoader和加载String的ClassLoader相同,而且javaseClassLoader的父加载器为null,所以为Bootstrap ClassLoader

Tomcat的类加载器双亲委派模型的目的是为了优先加载Web应用目录下的类,然后再加载其他目录下的类,这也为实现多个Web应用的隔离奠定了基础。这也符合Servlet规范的建议:全路径类名与系统类同名的话,优先加载Web应用自己定义的类

如果你并不想打破双亲委派模型,但是又想定义自己的类加载器来加载特定目录下的类,你需要重写findClass()loadClass()方法中的哪一个?还是两个都要重写?

:只需要重写findClass()方法,findClass()方法和loadClass()方法区别在于:findClass()方法负责具体的类加载细节,主要职责就是找到.class文件,可能来自文件系统或者网络,找到后把.class文件读到内存得到字节码数组,然后调用defineClass()方法得到Class对象。而loadClass()方法只是保证双亲委派机制运行流程,先看当前类是否被加载,若没有没加载交由父级类加载器加载,父类加载器加载失败由当前类加载器加载

3、Tomcat如何实现多个Web应用的隔离

Tomcat作为一个Web服务器需要解决以下问题:

  • 多个Web应用隔离类库:部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以互相独立使用
  • 多个Web应用共享类库:部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。例如用户可能有10个使用Spring的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过度膨胀的风险
  • 隔离Tomcat与Web应用的类:Tomcat服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响,服务器所使用的类库应该与应用程序的类库互相独立

1)、Tomcat类加载器的层次结构

为了解决这些问题,Tomcat 设计了类加载器的层次结构,它们的关系如下图:

在这里插入图片描述

WebAppClassLoader

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

SharedClassLoader

多个Web应用共享类库的问题本质是多个Web应用之间共享类库并且不能重复加载相同的类。在双亲委派模型里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗。SharedClassLoader作为WebAppClassLoader的父加载器专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类,就会委托父加载器SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,来解决共享的问题

CatalinaClassLoader

如何隔离Tomcat与Web应用的类要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此Tomcat又设计一个类加载器CatalinaClassLoader专门来加载Tomcat自身的类

CommonClassLoader

如果Tomcat和各Web应用之间需要共享一些类时该怎么办呢?CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器

CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离

2)、Spring的加载问题

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

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
    
    
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

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

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

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

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

Spring运行时懒加载业务bean,请求线程会触发依赖对象注入到IOC容器。因为应用程序执行的宿主环境就是Web容器,请求线程的类加载器自然就是WebAppClassLoader,Spring-Core包下ClassUtils.getDefaultClassLoader()代码如下:

public abstract class ClassUtils {
     
     
 
	public static ClassLoader getDefaultClassLoader() {
     
     
		ClassLoader cl = null;
		try {
     
     
			cl = Thread.currentThread().getContextClassLoader();
		}
		catch (Throwable ex) {
     
     
		}
		if (cl == null) {
     
     
			cl = ClassUtils.class.getClassLoader();
			if (cl == null) {
     
     
				try {
     
     
					cl = ClassLoader.getSystemClassLoader();
				}
				catch (Throwable ex) {
     
     
				}
			}
		}
		return cl;
	}

容器启动时,Spring注入Bean时的类加载器还是SharedClassLoader。初始加载bean和懒加载bean的驱动线程不同,一个是Spring应用程序自身,一个是业务线程

参考

https://time.geekbang.org/column/article/105110

https://time.geekbang.org/column/article/105711

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/115424399