Tomcat 轻量级应用服务器原理探秘 类加载器篇

白菜Java自习室 涵盖核心知识

1. 类加载机制

什么是 Java 的类加载机制?

.java 文件通过编译以后,读取到 Java 的方法区中,然后类加载器的加载、验证(class特定的格式)、准备(分配内存)、解析(将字节码的符号引用改成直接引用)、初始化(对象的初始化)的过程。

前置知识学习请参考作者的文章:Java工程师的进阶之路 JVM篇

2. 双亲委派机制

什么是 双亲委派机制?

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

Java类加载器.webp

BootstrapClassLoader(启动类加载器)

c++ 编写,加载 Java 核心库 java.*,构造 ExtClassLoader 和 AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

ExtClassLoader (标准扩展类加载器)

Java 编写,加载扩展库,如 classpath 中的 jre,javax.* 或者 java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)

Java 编写,加载程序所在的目录。

CustomClassLoader(用户自定义类加载器)

Java 编写,用户自定义的类加载器,可加载指定路径的 class 文件。

双亲委派机制的作用

  1. 防止重复加载同一个 .class,加载过了,就不用再加载一遍,保证数据安全。
  2. 保证了 JVM 提供的核心类不被篡改,保证 class 执行安全。

3. Java 8 中 ClassLoad 源码

如果开发人员需要自定义的加载器需要重新注意重写下面的 loadClass()findClass() 方法,从而可以打破双亲的委派机制。

ClassLoad 中的 loadClass() 方法


    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 是一个递归调用,直至没有父类的类加载器
                        c = parent.loadClass(name, false);
                    } else {
                        // 加载 Java 核心类库的加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 在指定的路径下获取到该类
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
复制代码

URLClassloader 中的 findClass() 方法


    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        // 拼接成指定前缀的.class文件
                        String path = name.replace('.', '/').concat(".class");
                        // 从 classpath 获取到.class文件
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                // 从路径中读取class文件字节流,并定义成类;todo bytebuffer
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
复制代码

4. Tomcat 如何打破双亲委派机制

下面的简图是 Tomcat的官方文档给出的类加载器的图:

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ..
复制代码
  • Bootstrap:是 Java 的最高的加载器,用 C 语言实现,主要用来加载 JVM 启动时所需要的核心类,例如 $JAVA_HOME/jre/lib/ext 路径下的类。
  • System:会加载 CLASSPATH 系统变量所定义路径的所有的类。
  • Common:会加载 Tomcat 路径下的 lib 文件下的所有类。
  • Webapp1、Webapp2:会加载 webapp 路径下项目中的所有的类。一个项目对应一个WebappClassLoader,这样就实现了应用之间类的隔离了。

那么 Tomcat 为什么要自定义类加载器呢?

  • 性能:如果在一个 Tomcat 部署多个应用,多个应用中都有相同的类库依赖。那么可以把这相同的类库让 Common 类加载器进行加载。
  • 灵活性:Web 应用之间的类加载器相互独立,那么就可以根据修改不同的文件重建不同的类加载器替换原来的,从而不影响其他应用。
  • 应用隔离:部署在同一个 Tomcat 中的不同应用 A 和 B,例如 A 用了 Spring 2.5,B 用了Spring 3.5,那么这两个应用如果使用的是同一个类加载器,那么 Web 应用就会因为 jar 包覆盖而无法启动。

Tomcat 打破双亲委派机制

Tomcat 自定义了 WebAppClassLoader 类加载器,打破了双亲委派的机制。即如果收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载 Web 应用自己定义的类

Tomcat 重写下面的 loadClass()findClass() 方法,从而可以打破双亲的委派机制。

ClassLoad 中的 loadClass() 方法

@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        // 1. 从本地缓存中查找是否加载过此类
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // 2. 从AppClassLoader中查找是否加载过此类
        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);
        // 3. 尝试用ExtClassLoader 类加载器加载类,防止Web应用覆盖JRE的核心类
        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) {
            tryLoadingFromJavaseLoader = true;
        }

        boolean delegateLoad = delegate || filter(name, true);

        // 4. 判断是否设置了delegate属性,如果设置为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
            }
        }

        // 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
        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
        }

        // 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
        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);
}
复制代码

URLClassloader 中的 findClass() 方法

 @Override
 public Class<?> findClass(String name) throws ClassNotFoundException {

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

        checkStateForClassLoading(name);

        // (1) Permission to define this class when using a SecurityManager
        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);
                }
            }
        }

        // Ask our superclass to locate this class, if possible
        // (throws ClassNotFoundException if it is not found)
        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 {
                    // 从本地的具体类名查找 内部方式
                    clazz = findClassInternal(name);
                }
            } catch(AccessControlException ace) {
                log.warn("WebappClassLoader.findClassInternal(" + name
                        + ") security exception: " + 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 {
                    // 调用父类的加载方式
                    clazz = super.findClass(name);
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoader.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("  -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            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;
        }

        // Return the class we have located
        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;
    }
复制代码

Web应用默认的类加载顺序是(打破了双亲委派规则)

  1. 先从 JVM 的 BootStrapClassLoader 中加载。
  2. 加载 Web 应用下 /WEB-INF/classes 中的类。
  3. 加载 Web 应用下 /WEB-INF/lib/*.jap 中的 jar 包中的类。
  4. 加载上面定义的 System 路径下面的类。
  5. 加载上面定义的 Common 路径下面的类。

如果在配置文件中配置了“,那么就是遵循双亲委派规则,加载顺序如下

  1. 先从 JVM 的 BootStrapClassLoader 中加载。
  2. 加载上面定义的 System 路径下面的类。
  3. 加载上面定义的 Common 路径下面的类。
  4. 加载 Web 应用下 /WEB-INF/classes 中的类。
  5. 加载 Web 应用下 /WEB-INF/lib/*.jap 中的 jar 包中的类。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享