tomcat类加载源码分析

在tomcat里某个应用中,每个应用包含1个类加载器WebappClassLoader,该应用的类都通过该类加载器加载,其loadClass方法如下:

@Override
public Class loadClass(String name) throws ClassNotFoundException {
    return (loadClass(name, false));
}

@Override
public synchronized Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException {

    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class clazz = null;

    // Log access to stopped classloader
    if (!started) {
        try {
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }

    // (0) Check our previously loaded local class cache
    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
    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 J2SE classes
    try {
        clazz = system.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);

    // (1) Delegate to our parent if requested
    if (delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = Class.forName(name, false, loader);
            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);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = Class.forName(name, false, loader);
            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);

}

step1:首先是在该WebappClassLoader缓存的类中检查要加载的类有没有缓存过,会调用findLoadedClass0方法:

protected Class findLoadedClass0(String name) {

    ResourceEntry entry = resourceEntries.get(name);
    if (entry != null) {
        return entry.loadedClass;
    }
    return (null);  // FIXME - findLoadedResource()

}

在WebappClassLoader中,缓存的类保存在resourceEntries这个成员里,它是1个map结构,key为类的名字,value为ResourceEntry:

protected HashMap resourceEntries = new HashMap();

ResourceEntry的成员如下:

成员 类型 说明
lastModified long 上次更新时间,用于热加载功能
binaryContent byte[] 类的字节数组
loadedClass Class 保存该类的类实例
source URL URL source from where the object was loaded.
codeBase URL URL of the codebase from where the object was loaded.
manifest Manifest Manifest (if the resource was loaded from a JAR).
certificates Certificate[] Certificates (if the resource was loaded from a JAR).

以我部署的web.war项目为例,对于/WEB-INF/classes/下的类,如自定义的com.meli.inc.tomcat.web.HelloController类,其ResourceEntry如下:

tomcat类加载源码分析_第1张图片
HelloController.png

对于/WEB-INF/lib/*.jar下的类,如org.springframework.context.i18n.LocaleContext,其ResourceEntry如下:

tomcat类加载源码分析_第2张图片
LocaleContext.png

其source成员的file和path如下:

file:/Users/cangxing/Documents/Study/tomcat/Tomcat/tomcat-7.0.42-sourcecode/webapps/web/WEB-INF/lib/spring-context-4.0.2.RELEASE.jar!/org/springframework/context/i18n/LocaleContext.class

以上ResourceEntry截图中,是在调用defineClass方法之前各个变量的值,loadedClass为null,实际上,在调用defineClass方法后,会把class字节码文件字节流转换为运行时class类对象,即loadedClass会被赋值为Class类型的该类类对象,然后把ResourceEntry其它成员置为null,因为该类已经被加载到了内存中,即loadedClass,其它成员没有用了,置为null将其gc掉。

step2:然后是检查jvm的类加载器中有没有缓存过该类的加载器,会调用父类ClassLoader的findLoadedClass方法:

protected final Class findLoadedClass(String name) {
    if (!checkName(name))
        return null;
    return findLoadedClass0(name);
}

在ClassLoader类中,findLoadedClass0为本地方法。

step3:然后用系统类加载器加载该类,防止J2SE的基础类被覆盖,会调用系统类加载器的loadClass方法。

step4:然后在本地仓库中寻找该类,会调用WebappClassLoader的findClass方法,如下:

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

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

    // Cannot load anything from local repositories if class loader is stopped
    if (!started) {
        throw new ClassNotFoundException(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 + ")");
        if (hasExternalRepositories && searchExternalFirst) {
            try {
                clazz = super.findClass(name);
            } catch(ClassNotFoundException cnfe) {
                // Ignore - will search internal repositories next
            } 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)) {
            try {
                clazz = findClassInternal(name);
            } catch(ClassNotFoundException cnfe) {
                if (!hasExternalRepositories || searchExternalFirst) {
                    throw cnfe;
                }
            } 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 && !searchExternalFirst) {
            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);

}

不考虑使用SecurityManager的情况,最终会调用findClassInternal方法,如下:

protected Class findClassInternal(String name)
    throws ClassNotFoundException {

    if (!validate(name))
        throw new ClassNotFoundException(name);

    String tempPath = name.replace('.', '/');
    String classPath = tempPath + ".class";

    ResourceEntry entry = null;

    if (securityManager != null) {
        PrivilegedAction dp =
            new PrivilegedFindResourceByName(name, classPath);
        entry = AccessController.doPrivileged(dp);
    } else {
        entry = findResourceInternal(name, classPath);
    }

    if (entry == null)
        throw new ClassNotFoundException(name);

    Class clazz = entry.loadedClass;
    if (clazz != null)
        return clazz;

    synchronized (this) {
        clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        if (entry.binaryContent == null)
            throw new ClassNotFoundException(name);

        // Looking up the package
        String packageName = null;
        int pos = name.lastIndexOf('.');
        if (pos != -1)
            packageName = name.substring(0, pos);

        Package pkg = null;

        if (packageName != null) {
            pkg = getPackage(packageName);
            // Define the package (if null)
            if (pkg == null) {
                try {
                    if (entry.manifest == null) {
                        definePackage(packageName, null, null, null, null,
                                null, null, null);
                    } else {
                        definePackage(packageName, entry.manifest,
                                entry.codeBase);
                    }
                } catch (IllegalArgumentException e) {
                    // Ignore: normal error due to dual definition of package
                }
                pkg = getPackage(packageName);
            }
        }

        if (securityManager != null) {

            // Checking sealing
            if (pkg != null) {
                boolean sealCheck = true;
                if (pkg.isSealed()) {
                    sealCheck = pkg.isSealed(entry.codeBase);
                } else {
                    sealCheck = (entry.manifest == null)
                        || !isPackageSealed(packageName, entry.manifest);
                }
                if (!sealCheck)
                    throw new SecurityException
                        ("Sealing violation loading " + name + " : Package "
                         + packageName + " is sealed.");
            }

        }

        try {
            clazz = defineClass(name, entry.binaryContent, 0,
                    entry.binaryContent.length,
                    new CodeSource(entry.codeBase, entry.certificates));
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    ucve.getLocalizedMessage() + " " +
                    sm.getString("webappClassLoader.wrongVersion",
                            name));
        }
        /**
         * 加载完某个类后把该类的二进制字节数组等信息置为null,帮助gc,只保留class
         */
        entry.loadedClass = clazz;
        entry.binaryContent = null;
        entry.source = null;
        entry.codeBase = null;
        entry.manifest = null;
        entry.certificates = null;
    }

    return clazz;

}

这里主要包含2步:
step4.1:entry = findResourceInternal(name, classPath);
这一步在WEB-INF/classes/和WEB-INF/lib/*.jar中根据类的name搜索,找到后封装成ResourceEntry返回。

protected ResourceEntry findResourceInternal(String name, String path) {

    if (!started) {
        log.info(sm.getString("webappClassLoader.stopped", name));
        return null;
    }

    if ((name == null) || (path == null))
        return null;

    ResourceEntry entry = resourceEntries.get(name);
    if (entry != null)
        return entry;

    int contentLength = -1;
    InputStream binaryStream = null;
    boolean isClassResource = path.endsWith(".class");

    int jarFilesLength = jarFiles.length;
    int repositoriesLength = repositories.length;

    int i;

    Resource resource = null;

    boolean fileNeedConvert = false;

    for (i = 0; (entry == null) && (i < repositoriesLength); i++) {
        try {

            String fullPath = repositories[i] + path;

            Object lookupResult = resources.lookup(fullPath);
            if (lookupResult instanceof Resource) {
                resource = (Resource) lookupResult;
            }

            // Note : Not getting an exception here means the resource was
            // found

            ResourceAttributes attributes =
                (ResourceAttributes) resources.getAttributes(fullPath);
            contentLength = (int) attributes.getContentLength();
            String canonicalPath = attributes.getCanonicalPath();
            if (canonicalPath != null) {
                // we create the ResourceEntry based on the information returned
                // by the DirContext rather than just using the path to the
                // repository. This allows to have smart DirContext implementations
                // that "virtualize" the docbase (e.g. Eclipse WTP)
                entry = findResourceInternal(new File(canonicalPath), "");
            } else {
                // probably a resource not in the filesystem (e.g. in a
                // packaged war)
                entry = findResourceInternal(files[i], path);
            }
            entry.lastModified = attributes.getLastModified();

            if (resource != null) {


                try {
                    binaryStream = resource.streamContent();
                } catch (IOException e) {
                    return null;
                }

                if (needConvert) {
                    if (path.endsWith(".properties")) {
                        fileNeedConvert = true;
                    }
                }

                // Register the full path for modification checking
                // Note: Only syncing on a 'constant' object is needed
                synchronized (allPermission) {

                    int j;

                    long[] result2 =
                        new long[lastModifiedDates.length + 1];
                    for (j = 0; j < lastModifiedDates.length; j++) {
                        result2[j] = lastModifiedDates[j];
                    }
                    result2[lastModifiedDates.length] = entry.lastModified;
                    lastModifiedDates = result2;

                    String[] result = new String[paths.length + 1];
                    for (j = 0; j < paths.length; j++) {
                        result[j] = paths[j];
                    }
                    result[paths.length] = fullPath;
                    paths = result;

                }

            }

        } catch (NamingException e) {
            // Ignore
        }
    }

    if ((entry == null) && (notFoundResources.containsKey(name)))
        return null;

    JarEntry jarEntry = null;

    synchronized (jarFiles) {

        try {
            if (!openJARs()) {
                return null;
            }
            for (i = 0; (entry == null) && (i < jarFilesLength); i++) {

                jarEntry = jarFiles[i].getJarEntry(path);

                if (jarEntry != null) {

                    entry = new ResourceEntry();
                    try {
                        entry.codeBase = getURI(jarRealFiles[i]);
                        String jarFakeUrl = entry.codeBase.toString();
                        jarFakeUrl = "jar:" + jarFakeUrl + "!/" + path;
                        entry.source = new URL(jarFakeUrl);
                        entry.lastModified = jarRealFiles[i].lastModified();
                    } catch (MalformedURLException e) {
                        return null;
                    }
                    contentLength = (int) jarEntry.getSize();
                    try {
                        entry.manifest = jarFiles[i].getManifest();
                        binaryStream = jarFiles[i].getInputStream(jarEntry);
                    } catch (IOException e) {
                        return null;
                    }

                    // Extract resources contained in JAR to the workdir
                    if (antiJARLocking && !(path.endsWith(".class"))) {
                        byte[] buf = new byte[1024];
                        File resourceFile = new File
                            (loaderDir, jarEntry.getName());
                        if (!resourceFile.exists()) {
                            Enumeration entries =
                                jarFiles[i].entries();
                            while (entries.hasMoreElements()) {
                                JarEntry jarEntry2 =  entries.nextElement();
                                if (!(jarEntry2.isDirectory())
                                    && (!jarEntry2.getName().endsWith
                                        (".class"))) {
                                    resourceFile = new File
                                        (loaderDir, jarEntry2.getName());
                                    try {
                                        if (!resourceFile.getCanonicalPath().startsWith(
                                                canonicalLoaderDir)) {
                                            throw new IllegalArgumentException(
                                                    sm.getString("webappClassLoader.illegalJarPath",
                                                jarEntry2.getName()));
                                        }
                                    } catch (IOException ioe) {
                                        throw new IllegalArgumentException(
                                                sm.getString("webappClassLoader.validationErrorJarPath",
                                                        jarEntry2.getName()), ioe);
                                    }
                                    File parentFile = resourceFile.getParentFile();
                                    if (!parentFile.mkdirs() && !parentFile.exists()) {
                                        // Ignore the error (like the IOExceptions below)
                                    }
                                    FileOutputStream os = null;
                                    InputStream is = null;
                                    try {
                                        is = jarFiles[i].getInputStream
                                            (jarEntry2);
                                        os = new FileOutputStream
                                            (resourceFile);
                                        while (true) {
                                            int n = is.read(buf);
                                            if (n <= 0) {
                                                break;
                                            }
                                            os.write(buf, 0, n);
                                        }
                                        resourceFile.setLastModified(
                                                jarEntry2.getTime());
                                    } catch (IOException e) {
                                        // Ignore
                                    } finally {
                                        try {
                                            if (is != null) {
                                                is.close();
                                            }
                                        } catch (IOException e) {
                                            // Ignore
                                        }
                                        try {
                                            if (os != null) {
                                                os.close();
                                            }
                                        } catch (IOException e) {
                                            // Ignore
                                        }
                                    }
                                }
                            }
                        }
                    }

                }

            }

            if (entry == null) {
                synchronized (notFoundResources) {
                    notFoundResources.put(name, name);
                }
                return null;
            }

            /* Only cache the binary content if there is some content
             * available and either:
             * a) It is a class file since the binary content is only cached
             *    until the class has been loaded
             *    or
             * b) The file needs conversion to address encoding issues (see
             *    below)
             *
             * In all other cases do not cache the content to prevent
             * excessive memory usage if large resources are present (see
             * https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
             */
            if (binaryStream != null &&
                    (isClassResource || fileNeedConvert)) {

                byte[] binaryContent = new byte[contentLength];

                int pos = 0;
                try {

                    while (true) {
                        int n = binaryStream.read(binaryContent, pos,
                                                  binaryContent.length - pos);
                        if (n <= 0)
                            break;
                        pos += n;
                    }
                } catch (IOException e) {
                    log.error(sm.getString("webappClassLoader.readError", name), e);
                    return null;
                }
                if (fileNeedConvert) {
                    // Workaround for certain files on platforms that use
                    // EBCDIC encoding, when they are read through FileInputStream.
                    // See commit message of rev.303915 for details
                    // http://svn.apache.org/viewvc?view=revision&revision=303915
                    String str = new String(binaryContent,0,pos);
                    try {
                        binaryContent = str.getBytes(CHARSET_UTF8);
                    } catch (Exception e) {
                        return null;
                    }
                }
                /**
                 * 类的二进制字节数组
                 */
                entry.binaryContent = binaryContent;

                // The certificates are only available after the JarEntry
                // associated input stream has been fully read
                if (jarEntry != null) {
                    entry.certificates = jarEntry.getCertificates();
                }

            }
        } finally {
            if (binaryStream != null) {
                try {
                    binaryStream.close();
                } catch (IOException e) { /* Ignore */}
            }
        }
    }

    // Add the entry in the local resource repository
    synchronized (resourceEntries) {
        // Ensures that all the threads which may be in a race to load
        // a particular class all end up with the same ResourceEntry
        // instance
        ResourceEntry entry2 = resourceEntries.get(name);
        if (entry2 == null) {
            /**
             * 对类进行缓存,下次用到直接返回
             */
            resourceEntries.put(name, entry);
        } else {
            entry = entry2;
        }
    }

    return entry;

}

step4.2:clazz = defineClass(name, entry.binaryContent, 0,
entry.binaryContent.length,
new CodeSource(entry.codeBase, entry.certificates));
4.1中找到了该类并且拿到了该类的字节数组,step4.2调用ClassLoader的defineClass方法,将该类的字节数组转换为jvm能够识别的运行时class对象。

通过上述源码分析,tomcat类加载的规则如下:
(1)首先检查WebappClassLoader加载的类缓存中有没有加载过该类,如果有则返回,否则进入(2);
(2)从jvm的系统类加载器的类缓存中检查有没有加载过该类,如果有则返回,否则进入(3);
(3)通过系统类加载器加载该类,如果能加载到则返回,负责进入(4);
(4)在/WEB-INF/classes/和/WEB-INF/lib/*.jar中寻找该类,如果寻找到该类,把该类对应的class字节码文件转换为字节流,然后调用ClassLoader的defineClass方法将该类字节码文件的字节流转换为运行期class类对象,完成类的加载,否则抛出ClassNotFoundException。

附:WebappClassLoader类的成员:

成员 类型 说明
resources DirContext 该应用的资源对象
resourceEntries HashMap 该应用所有缓存的类及对应的ResourceEntry
notFoundResources LinkedHashMap 该应用所有找不到的类缓存,当下次加载某个类时如果在notFoundResources中找不到,直接抛出ClassNotFoundException。
delegate boolean 在从该应用的仓库加载类之前是否首先代理给父加载器加载,默认为false,即优先从本应用的类仓库加载该类
lastJarAccessed long Last time a JAR was accessed.
repositories String[] 该应用的类仓库,一般只有/WEB-INF/classes/
repositoryURLs URL[] 该应用所有类所在目录的URL
files File[] 应用自身的类所在文件目录,不包含依赖的jar,一般为/WEB-INF/classes
jarRealFiles File[] 应用依赖的所有jar包的路径
jarPath String jar文件监控路径,一般为/WEB-INF/lib
jarNames String[] jar文件名列表
lastModifiedDates long[] 这个数组的大小与paths一致,对应paths中每个jar或class的更新时间戳
paths String[] 所有类资源的路径,包含/WEB-INF/lib/下的所有jar和/WEB-INF/classes/下的所有class
loaderDir File Path where resources loaded from JARs will be extracted.
canonicalLoaderDir String Path where resources loaded from JARs will be extracted.
loaderPC HashMap The PermissionCollection for each CodeSource for a web application context.
parent ClassLoader 父加载器。
system ClassLoader 系统类加载器,即tomcat的应用类加载器,类加载路径为$CATALINA_HOME/bin/下的类
started boolean 组件是否已经启动。
contextName String Name of associated context used with logging and JMX to associate with the right web application. Particularly useful for the clear references messages. Defaults to unknown but if standard Tomcat components are used it will be updated during initialisation from the resources.

resources成员的变量如下:


tomcat类加载源码分析_第3张图片
resources.png

resourceEntries成员如下,它是1个k->v结构,key是类的相对WEB-INF/classes/或WEB-INF/lib/.jar的相对路径文件名,value是ResourceRntry,这个成员维持着该应用已经加载了的类的缓存,下次加载如果能从该缓存中找到该类则直接用缓存的类,同样,在热部署机制中,当扫描到/WEB-INF/classes/和/WEB-INF/lib/.jar有文件的时间戳更新,了则会停止WebappClassLoader并重新启动WebappClassLoader,停止的过程中会清空resourceEntries。

tomcat类加载源码分析_第4张图片
resourceEntries.png

repositories成员如下,这个数组一般只包含1个成员"/WEB-INF/classes/"。


repositories.png

repositoryURLs成员是该应用的类路径,包括/WEB-INF/classes/和/WEB-INF/lib/下的所有jar包。


tomcat类加载源码分析_第5张图片
repositoryURLs.png

files是应用自身的类路径,如下:


files.png

jarRealFiles是该应用依赖的所有jar包的绝对路径,如下:


tomcat类加载源码分析_第6张图片
jarRealFiles.png

jarPath如下:


jarPath.png

jarNames如下:


tomcat类加载源码分析_第7张图片
jarNames.png

paths如下,包含/WEB-INF/classes/下的类路径和WEB-INF/lib/下的jar路径:


tomcat类加载源码分析_第8张图片
paths.png

loaderDir和canonicalLoaderDir如下:


loaderDir.png
canonicalLoaderDir.png

contextName如下:

contextName.png

你可能感兴趣的:(tomcat类加载源码分析)