Tomcat

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Tomcat > Tomcat类加载机制

一篇文章讲透Tomcat的类加载机制

作者:马坤鹏 / 孙玄

Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类,各个web应用自己的类加载器会优先加载,加载不到时再交给commonClassLoader走双亲委托,这篇文章主要给大家介绍了如何通过一篇文章讲透Tomcat的类加载机制的相关资料,需要的朋友可以参考下

-     前言     -

你了解 Apache Tomcat 的类加载机制吗?本文将从底层原理切入,彻底揭秘 Tomcat 类加载所涉及的源码、机制和方案,助你深入掌握 Tomcat 类加载核心!

-     JVM 类加载器     -

1、JVM类加载器

说起 Tomcat 类加载器,就不得不先简单说一下 JVM 类加载器,如下图所示:

这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。

双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。

这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

这里请注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。

2、类加载器的源码

public abstract class ClassLoader {
  //  每个类加载器都有一个父加载器
  private final ClassLoader parent;
  public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
     protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
           // 如果没有加载过
            if (c == null) {
                if (parent != null) {
                  //  先委托给父加载器去加载,注意这是个递归调用
                 c = parent.loadClass(name, false);
                } else {
                 // 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
                   c = findBootstrapClassOrNull(name);
                }
              
            // 如果父加载器没加载成功,调用自己的 findClass 去加载
                if (c == null) {        
                    c = findClass(name);
                }
            } 
        
            return c;
        }
        
    }
    //ClassLoader 中findClass方式需要被子类覆盖,下面这段代码就是对应代码
      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){
    
    }
    
}

我们自定义类加载器就需要重写ClassLoader的loadClass方法。

-     Tomcat 的类加载机制     -

1、加载机制的特点

隔离性:Web应用类库相互隔离,避免依赖库或者应用包相互影响。设想一下,如果我们 有两个Web应用,一个釆用了Spring 2.5, 一个采用了Spring 4.0,而应用服务器使用一个 类加载器加载,那么Web应用将会由于Jar包覆盖而导致无法启动成功;

灵活性:既然Web应用之间的类加载器相互独立,那么我们就能只针对一个Web应用进行 重新部署,此时该Web应用的类加载器将会重新创建,而且不会影响其他Web应用。如果 釆用一个类加载器,显然无法实现,因为只有一个类加载器的时候,类之间的依赖是杂 乱无章的,无法完整地移除某个Web应用的类;

性能:由于每个Web应用都有一个类加载器,因此Web应用在加载类时,不会搜索其他 Web应用包含的Jar包,性能自然高于应用服务器只有一个类加载器的情况。

2、Tomcat 的类加载方案

tomcat 8.5 默认改变了严格的双亲委派机制:

3、分析应用类加载器的加载过程

应用类加载器为WebappClassLoader ,他的loadClass在他的父类WebappClassLoaderBase中。

  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);    
            //从当前ClassLoader的本地缓存中加载类,如果找到则返回
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
            // 本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
            String resourceName = binaryNameToPath(name, false);
            //此时的javaseClassLoader是扩展类加载器  是把扩展类加载器赋值给了javaseClassLoader
            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
              .....
            //如果可以用getResource得到
            //如果能用扩展类加载器的getResource得到就证明可以被扩展类加载器加载到接下来安排扩展类加载器加载
            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
            //如果是true就是用父类加载器进行加载
            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
                }
            }
            // (2) Search local repositories
            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);
    }

注:在37行英文注释中标注获取的是系统类加载器,但我们debug的时候会发现他是扩展类加载器,实际中我们可以推断出他应该是扩展类加载器,因为如果我们加载的类在扩展类加载器路径下已经存在的话,那我们直接调用系统类加载器是就是错误的了,下图为debug后获取的类加载器的验证。

总结

tomcat打破了双亲委派的原则,实际是在应用类加载器中打破了双亲委派,其他类加载器还是遵循双亲委派的。

到此这篇关于Tomcat类加载机制的文章就介绍到这了,更多相关Tomcat类加载机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文