类装载器
JDK中提供了3种不同的类加载器:启动类装载器,扩展类装载器和系统类装载器。引导类装载器,用于引导启动Java虚拟机,当执行一个JAVA程序时,就会启动引导类装载器,它是使用本地代码来实现的,会装载%JAVA_HOME%\\jre\lib\rt.jar,它是所有类装载器类的父装载器。扩展类装载器负责载入标准扩展目录中的类,其搜索路径是%JAVA_HOME%\jre\lib\ext,只需要将打包好的jar文件放入这个目录就可以了,给开发提供了很大的便利性。系统类装载器是默认的装载器,其搜索路径是classpath。
JVM到底使用的是哪一个类装载器,取决于类装载器的代理模式。每当需要装载一个类的时候,会首先调用系统类装载器,但是系统类装载器并不会立即装载,而是将其交给父装载器:扩展类装载器,扩展类装载器其将交给引导类装载器,引导类装载器没有父装载器,它会尝试装载这个类,如果找不到这个类,会交给扩展类装载器,如果扩展类装载器还是没有找到,会交给系统类装载器,如果系统类装载器还是没有找到这个类,则会抛出java.lang.ClassNotFoundException异常。代理模式主要是为了解决类装载的安全问题。例如:对于自定类的java.lang.Object类,永远得不到装载,除非,rt.jar中确实没有这个类。
tomcat也提供了几种不同的类装载器用于加载不同位置的jar包和class文件,特别是Context容器需要有一个单独的类装载器,因为不同应用可能有相同的类,如果用同一个类装载器去装载,就不知道该加载哪个应用里面的类了。这些类装载器之间的关系如下图所示:
系统类装载器
tomcat的系统类装载器和JDK的系统类装载器有点不同的地方是搜索路径并不相同,在catalina.bat中做了如下修改:
- rem Add on extra jar file to CLASSPATH
- rem Note that there are no quotes as we do not want to introduce random
- rem quotes into the CLASSPATH
- if "%CLASSPATH%" == "" goto emptyClasspath
- set "CLASSPATH=%CLASSPATH%;"
- :emptyClasspath
- set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar"
- rem Add tomcat-juli.jar to classpath
- rem tomcat-juli.jar can be over-ridden per instance
- if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome
- set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar"
先将classpath清空,因为classpath中可能有tomcat启动相关的类会影响tomcat的正常启动。然后将bootstrap.jar和tomcat-juli.jar加入classpath中,在catalina.bat中调用了Bootstrap类的main方法,这里系统类装载器会装载Bootstrap类,Bootstrap类用到的Catalina类也是由系统类装载器装载的。
随后在Bootstrap的init方法中创建了3个类装载器:
- public void init() throws Exception{
-
- setCatalinaHome();
- setCatalinaBase();
-
- initClassLoaders();
-
- Thread.currentThread().setContextClassLoader(catalinaLoader);
-
- SecurityClassLoad.securityClassLoad(catalinaLoader);
- ...
- }
-
- private void initClassLoaders() {
- try {
- commonLoader = createClassLoader("common", null);
- if( commonLoader == null ) {
-
- commonLoader=this.getClass().getClassLoader();
- }
- catalinaLoader = createClassLoader("server", commonLoader);
- sharedLoader = createClassLoader("shared", commonLoader);
- } catch (Throwable t) {
- handleThrowable(t);
- log.error("Class loader creation threw exception", t);
- System.exit(1);
- }
- }
Common Class Loader
首先创建CommonLoader,是在createClassLoader方法中完成的,代码如下:
- private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {
- String value = CatalinaProperties.getProperty(name + ".loader");
- if ((value == null) || (value.equals("")))
- return parent;
-
- value = replace(value);
-
- List<Repository> repositories = new ArrayList<Repository>();
- StringTokenizer tokenizer = new StringTokenizer(value, ",");
- while (tokenizer.hasMoreElements()) {
- String repository = tokenizer.nextToken().trim();
- if (repository.length() == 0) {
- continue;
- }
-
-
- try {
- @SuppressWarnings("unused")
- URL url = new URL(repository);
- repositories.add(new Repository(repository, RepositoryType.URL));
- continue;
- } catch (MalformedURLException e) {
-
- }
-
-
- 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));
- }
- }
-
- ClassLoader classLoader = ClassLoaderFactory.createClassLoader(repositories, parent);
- ...
-
- return classLoader;
- }
(1)、从bootstrap.jar包可以找到catalina.properties文件:common.loader=${catalina.home}/lib,${catalina.home}/lib/*.jar,很明显这个类装载器搜索路径就是${catalina.home}/lib和${catalina.home}/lib/*.jar,之所以叫common class loader,是因为它加载每个应用要用到的公共jar包和class文件;
(2)、遍历values,将每个value封装成Repository,然后根据repository和parent创建这个classLoader,在这个方法中parent传入的是null值,代表这个类装载器的父装载器是系统类装载器,实际上返回的是StandardClassLoader类,StandardClassLoader类是URLClassLoader的子类,即将被废弃。之所以返回的是StandardClassLoader是在ClassLoaderFactory的createClassLoader方法中被包装了一层。
Catalina Class Loader
common class loader创建好之后,又创建了catalinaLoader,其搜索路径为空,以下是catalina.properties的配置项:
- common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
- server.loader=
- shared.loader=
common class loader是以common class loader为父装载器的,因此其搜索路径和common class loader一样。catalina class loader创建好后,在init方法中随即调用了Thread.currentThread().setContextClassLoader(catalinaLoader);将其设置为当前线程的类装载器
已不再使用,tomcat早期的版本在使用这个类装载器,负责装载应用中公用的类,后来这些公用的类被移到了{catalina.base}/lib目录下,这个装载器暂时未被使用
综上所述:tomcat在启动的时候初始化了三个类加载器,commonLoader,catalinaLoader,sharedLoader.其中commonLoader是另外两个的父装载器,且为standardClassLoader类型,tomcat真正使用的是commonLoader,engine,host,connector等都是使用commonLoader装载的。
Webapp Class Loader
这个类装载器是tomcat自定义的类装载器,先来看看类图:
tomcat的一个service除了容器和连接器外还有很多组件,比如sessionManager,logger,loader等,这个类装载器是以组件的形式附着在每个容器上的,Engine和Host的这两个容器的loader组件为null,context里面是有值的,看看context的startInternal方法:
- protected synchronized void startInternal() throws LifecycleException {
- ...
- if (getLoader() == null) {
- WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
- webappLoader.setDelegate(getDelegate());
- setLoader(webappLoader);
- }
- ...
-
- ClassLoader oldCCL = bindThread();
- try {
-
- if (ok) {
-
-
- if ((loader != null) && (loader instanceof Lifecycle))
- ((Lifecycle) loader).start();
-
-
-
-
-
- unbindThread(oldCCL);
- oldCCL = bindThread();
- }
- ...
- } finally {
-
- unbindThread(oldCCL);
- }
很明显,context在启动的时候创建了一个loader组件,webapploader正是loader的实现类,这个类并不是最终的类装载器,在这个类里面有一个webappclassloader类型的字段叫classloader,这个classloader的创建是在loader组件的start方法中完成的
- protected void startInternal() throws LifecycleException {
- ...
-
- try {
- classLoader = createClassLoader();
- classLoader.setResources(container.getResources());
- classLoader.setDelegate(this.delegate);
- classLoader.setSearchExternalFirst(searchExternalFirst);
- if (container instanceof StandardContext) {
- classLoader.setAntiJARLocking(
- ((StandardContext) container).getAntiJARLocking());
- classLoader.setClearReferencesStatic(
- ((StandardContext) container).getClearReferencesStatic());
- classLoader.setClearReferencesStopThreads(
- ((StandardContext) container).getClearReferencesStopThreads());
- classLoader.setClearReferencesStopTimerThreads(
- ((StandardContext) container).getClearReferencesStopTimerThreads());
- classLoader.setClearReferencesHttpClientKeepAliveThread(
- ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
- }
-
- for (int i = 0; i < repositories.length; i++) {
- classLoader.addRepository(repositories[i]);
- }
-
-
- setRepositories();
- setClassPath();
-
- setPermissions();
-
- ((Lifecycle) classLoader).start();
- ...
- } catch (Throwable t) {
- ...
- }
- ...
- }
(1)、在createClassLoader方法中通过反射实例化了org.apache.catalina.loader.WebappClassLoader这个类,并调用了它的setParentClassLoader设置其父装载器为standardClassLoader
- private WebappClassLoader createClassLoader()
- throws Exception {
-
- Class<?> clazz = Class.forName(loaderClass);
- WebappClassLoader classLoader = null;
-
- if (parentClassLoader == null) {
- parentClassLoader = container.getParentClassLoader();
- }
- Class<?>[] argTypes = { ClassLoader.class };
- Object[] args = { parentClassLoader };
- Constructor<?> constr = clazz.getConstructor(argTypes);
- classLoader = (WebappClassLoader) constr.newInstance(args);
-
- return classLoader;
-
- }
(2)、为webappclassloader添加仓库(仓库表示类装载器会在哪些路径搜索类)
将/WEB-INF/classes目录添加到仓库中,然后将/WEB-INF/lib目录下的jar包也添加到仓库中
- private void setRepositories() throws IOException {
- ...
-
- String classesPath = "/WEB-INF/classes";
-
- ...
-
- classLoader.addRepository(classesPath + "/", classRepository);
-
-
- String libPath = "/WEB-INF/lib";
- ...
-
- NamingEnumeration<NameClassPair> enumeration = libDir.list("");
- while (enumeration.hasMoreElements()) {
- NameClassPair ncPair = enumeration.nextElement();
- String filename = libPath + "/" + ncPair.getName();
- if (!filename.endsWith(".jar"))
- continue;
- ...
- try {
- JarFile jarFile = new JarFile(destFile);
- classLoader.addJar(filename, jarFile, destFile);
- } catch (Exception ex) {
- ...
- }
- ...
- }
- }
(3)、为类装载器设置权限,这里Globals.IS_SECURITY_ENABLED值为false,表示安全机制未打开,直接返回
(4)、启动这个loader
Webappclassloader设计的过程中考虑了优化和安全两方面。例如,它会缓存之前已经载入的类以提高性能。此外,它还会缓存失败的类的名字,下次再次请求加载相同的类时直接抛出ClassNotFoundException异常。考虑到安全性,不允许载入指定的某些类,这些类在triggers数组中,目前有两个类:
- protected static final String[] triggers = {
- "javax.servlet.Servlet", "javax.el.Expression"
- };
WebappClassLoader装载类
loadClass是在其loadClass方法中完成的,下面详细分析这个方法:
整个思路是:先到缓存中获取,如果缓存中有直接返回,否则根据delegateLoad采取不同的加载方式。如果未启用这个标志:先本地仓库加载再父装载器或者系统类装载器装载;如果启用了这个标志:直接由父装载器或者系统类装载器装载。
类缓存
tomcat之所以采用自定义类装载器,除了不同应用之间有相同类不好解决之外,还有一个原因是可以缓存类以提高速度。每个由webappclassloader装载的类被视为资源,用ResourceEntry表示。加入缓存的代码是在loadclass方法中完成的,前面提到会搜索本地仓库,就是在这步调用了findClass方法完成了类的查找,并把找到的类封装成ResourceEntry,最后把这个resourceEntry放入resourceEntries中缓存起来。