Tomcat-类加载机制

Tomcat ClassLoader 加载,还是有必要唠一唠,文中使用的 Tomcat-8.5.30 版本。

Tomcat ClassLoader

Tomcat 的启动是从 Boostrap 类的 main 方法开始,调用 bootstrap.init

// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
    bootstrap.init();
} catch (Throwable t) {
    handleThrowable(t);
    t.printStackTrace();
    return;
}

然后调用 Bootstrap 类的 initClassLoader 方法

private void initClassLoaders() {
    try {
        //(1) 创建 commonLoader
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        //(2) 创建 catalinaLoader,父类加载器为 conmonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        //(3) 创建 sharedLoader,父类加载器为 commonLoader
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

之后,我们进行深入 createClassLoader 方法:

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    //(4) 获取catalina.propertties 中配置项分别为:common.loader、service.loader、shared.loader
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    //(5) 配置装饰类加载器所需的扫描路径
    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(
                new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(
                new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(
                new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(
                new Repository(repository, RepositoryType.DIR));
        }
    }

    //(6) 具体创建类加载器
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

在代码(4)中,根据配置项用来设置对应类加载器扫描类的路径,默认为:

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=

同时也可以根据可知,commonLoader、serverLoader、sharedLoader 是同一个 ClassLoader。

然后具体的创建类加载器在第六步,分析 createClassLoader 方法:

public static ClassLoader createClassLoader(List<Repository> repositories,
                                            final ClassLoader parent)
    throws Exception {

    if (log.isDebugEnabled())
        log.debug("Creating new class loader");

    // Construct the "class path" for this class loader
    Set<URL> set = new LinkedHashSet<>();

    if (repositories != null) {
        for (Repository repository : repositories)  {
            if (repository.getType() == RepositoryType.URL) {
                URL url = buildClassLoaderUrl(repository.getLocation());
                if (log.isDebugEnabled())
                    log.debug("  Including URL " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.DIR) {
                File directory = new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.DIR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(directory);
                if (log.isDebugEnabled())
                    log.debug("  Including directory " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.JAR) {
                File file=new File(repository.getLocation());
                file = file.getCanonicalFile();
                if (!validateFile(file, RepositoryType.JAR)) {
                    continue;
                }
                URL url = buildClassLoaderUrl(file);
                if (log.isDebugEnabled())
                    log.debug("  Including jar file " + url);
                set.add(url);
            } else if (repository.getType() == RepositoryType.GLOB) {
                File directory=new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.GLOB)) {
                    continue;
                }
                if (log.isDebugEnabled())
                    log.debug("  Including directory glob "
                              + directory.getAbsolutePath());
                String filenames[] = directory.list();
                if (filenames == null) {
                    continue;
                }
                for (int j = 0; j < filenames.length; j++) {
                    String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                    if (!filename.endsWith(".jar"))
                        continue;
                    File file = new File(directory, filenames[j]);
                    file = file.getCanonicalFile();
                    if (!validateFile(file, RepositoryType.JAR)) {
                        continue;
                    }
                    if (log.isDebugEnabled())
                        log.debug("    Including glob jar file "
                                  + file.getAbsolutePath());
                    URL url = buildClassLoaderUrl(file);
                    set.add(url);
                }
            }
        }
    }

    // Construct the class loader itself
    final URL[] array = set.toArray(new URL[set.size()]);
    if (log.isDebugEnabled())
        for (int i = 0; i < array.length; i++) {
            log.debug("  location " + i + " is " + array[i]);
        }
    //(7) 创建类加载器
    return AccessController.doPrivileged(
        new PrivilegedAction<URLClassLoader>() {
            @Override
            public URLClassLoader run() {
                if (parent == null)
                    return new URLClassLoader(array);
                else
                    return new URLClassLoader(array, parent);
            }
        });
}

从第七步,创建可知,创建 commonLoader 时,若parent == null 这个时候使用了单个参数的 URLClassLoader 创建了 URLClassLoader 类加载器作为 commonLoader,而URLClassLoader默认的父加载器为 AppClassLoader。

总结:

  • 默认情况下,Tomcat 中的 commonLoader、sharedLoader、catAlinaLoader 是同一个加载器,其他查找路径都使同一个地方。
  • catalinaLoader:主要加载Tomcat本身启动所需要的类
  • shareLoader:是 webAPPClassLoader 的父类,加载所有应用所需要的类
  • commonLoader:作为 sharedLoader 和catalinaLoader的父类,主要是加载二者共享的类

Tomcat 内部构造

Tomcat 内部构造图:

image.png

对上图分析:

  • Engine 是最大的容器,默认实现是 StandardEngine,里面可以包含若干个 Host。
  • Host 的默认实现为 StandardHost,Host 的父容器为 Engine。
  • 每个 Host 容器内部有若干个 Context 容器,默认为 StandardContext。
  • context 容器的父容器为 Host,每个 context 代表一个应用。
  • 而创建 WebAPPClassLoader 就是在 StandardContext 的 startInternal 方法。

startInternal 方法的代码如下:

protected synchronized void startInternal() throws LifecycleException {
        ...
        //(8) 创建 WebappLoader 对象,并调用 getparentClassLoader() 获取到 shardLoader
        if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

           ...
        try {
            if (ok) {
                //(9) 调用 WebappLoader 的start 方法创建当前应用的 WebappClassLoader 加载器
                Loader loader = getLoader();
                if (loader instanceof Lifecycle) {
                    ((Lifecycle) loader).start();
                }
                ...
            }
        }
}

然后调用的是 LifecycleBase 类中 start 然后执行 startInternal() 方法,追踪到 WebAppLoader

@Override
protected void startInternal() throws LifecycleException {

    if (log.isDebugEnabled())
        log.debug(sm.getString("webappLoader.starting"));

    if (context.getResources() == null) {
        log.info("No resources for " + context);
        setState(LifecycleState.STARTING);
        return;
    }

    // Construct a class loader based on our current repositories list
    try {
        //(10) 调用 createClassLoader,创建类加载器 
        classLoader = createClassLoader();
        classLoader.setResources(context.getResources());
        classLoader.setDelegate(this.delegate);

        // Configure our repositories
        setClassPath();

        setPermissions();

        ((Lifecycle) classLoader).start();

        String contextName = context.getName();
        if (!contextName.startsWith("/")) {
            contextName = "/" + contextName;
        }
        ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
                                            classLoader.getClass().getSimpleName() + ",host=" +
                                            context.getParent().getName() + ",context=" + contextName);
        Registry.getRegistry(null, null)
            .registerComponent(classLoader, cloname, null);

    } catch (Throwable t) {
        t = ExceptionUtils.unwrapInvocationTargetException(t);
        ExceptionUtils.handleThrowable(t);
        log.error( "LifecycleException ", t );
        throw new LifecycleException("start: ", t);
    }

    setState(LifecycleState.STARTING);
}

查看 createClassLoader 方法:

private WebappClassLoaderBase createClassLoader()
    throws Exception {
    //(11) 创建并实例化一个 WebappClassLoader,并设置父类为:sharedLoader
    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

至此,创建了应用的类加载器,对于每个 StandardContext 对应一个 web 应用,所以不同的应用都有不同的 WebappClassLoader,共同点就是他们的父加载器都是 sharedLoader。

screenshot.png

WebappClassLoader 类加载原理

WebappClassLoaderBase 类中的 loadClass 方法

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

        // (0) Check our previously loaded local class cache
        // (12) 检查WebappClassLoader 缓存中是否已经加载该类
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }

        // (0.1) Check our previously loaded class cache
        // (13) 看jvm缓存中是否已经加载该类
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }

        // (0.2) Try loading the class with the system class loader, to prevent
        //       the webapp from overriding Java SE classes. This implements
        //       SRV.10.7.2
        // (14) 为了避免webapp覆盖Java SE classes,这里尝试使用 ExtClassLoader 进行加载
        String resourceName = binaryNameToPath(name, false);

        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
            // Use getResource as it won't trigger an expensive
            // ClassNotFoundException if the resource is not available from
            // the Java SE class loader. However (see
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
            // details) when running under a security manager in rare cases
            // this call may trigger a ClassCircularityError.
            // See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
            // details of how this may trigger a StackOverflowError
            // Given these reported errors, catch Throwable to ensure any
            // other edge cases are also caught
            tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
        } catch (Throwable t) {
            // Swallow all exceptions apart from those that must be re-thrown
            ExceptionUtils.handleThrowable(t);
            // The getResource() trick won't work for this class. We have to
            // try loading it directly and accept that we might get a
            // ClassNotFoundException.
            tryLoadingFromJavaseLoader = true;
        }
        // (15)
        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);
                }
            }
        }

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

        // (1) Delegate to our parent if requested
        // (17) 是否设置了委托给父类加载
        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
        // (18) webapp 本地搜索
        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
        // (19) 如果17步中没有委托给父类加载器加载,此时再调用父类加载器进行加载,
        //(违背父类加载器委托模型)
        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);
}

第14步:Tomcat 8 做了个优化,就是先调用 getResource 判断要加载的类在 ExtClassLoader 搜索路径下是否存在(getResource 的调用不会产生昂贵的代价),存在才调用 loadClass 进行加载

参考链接:http://gitbook.cn/books/5a7719e7367c47172bea2b53/index.html


文章作者: HoldDie
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 HoldDie !
评论
  目录