看 Tomcat 的源码越看越有趣。Tomcat 的代码总有一种处处都有那么一点调皮的感觉。今天就聊一聊 Tomcat 的类加载机制。
了解过 JVM 的类加载一定知道,JVM 类加载的双亲委派机制。但是 Tomcat 却打破了 JVM 固有的双亲委派加载机制。
JVM 的类加载
首先需要明确一下类加载是什么?
- Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。
JVM 预定义的三个加载器:
- 启动类加载器(Bootstrap ClassLoader):是用本地代码实现的类装入器,它负责将
<Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 标准扩展类加载器(Extension ClassLoader):是由 Sun 的
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
- 应用程序类加载器(Application ClassLoader):是由 Sun 的
AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
双亲委派机制:
所谓双亲委派机制,这里要指出的是,其实双亲委派来源于英文的 ”parents delegate“,仅仅表示的只是”父辈“,可见翻译的人不但英文是半吊子,而且也不了解 JVM 的类加载策略,造成了很大的误解。尤其是这个”双“字在初学的时候给我造成了极大的干扰。所以换个说法,应该是”父辈代理“。
类加载的时候,把加载的这个动作递归的委托给父辈,由父辈代劳,只有父辈无法加载时,才会由自己加载。
双亲委派加载模型:

这里需要特别注意的是加载器的关系并非是继承的关系。我们看代码:
1 2 3 4 5 6
| static class ExtClassLoader extends URLClassLoader{ ... ... } static class AppClassLoader extends URLClassLoader{ ... ... }
|
二者同时继承了 URLClassLoader ,继承关系如下:

怎么实现委托机制呢?在 ClassLoader 里面有几处比较重要的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public abstract class ClassLoader { private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { }
if (c == null) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
protected Class<?> findClass(String name) throws ClassNotFoundException { return defineClass(name, res); }
}
|
- 首先在初始化 ClassLoader 的时候需要指定自己的 parent 是谁?(这很重要)
- 先检查类有没被加载,如果类已经被加载了,直接返回。
- 如果没有被加载,则通过 parent 的 loadClass 来尝试加载类。(双亲委派的核心逻辑)
- 找不到 parent 的时候使用 bootstrap ClassLoader 进行加载。
- 如果委托的 parent 没法加载类,那就自己加载。
Tomcat 自己实现了自己的类加载器 WebAppClassLoader。类图关系图如下:

我们就来看看 Tomcat 的类加载器是怎么打破双亲委派的机制的。我们先看代码:
findClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Override public Class<?> findClass(String name) throws ClassNotFoundException { Class<?> clazz = null;
clazz = findClassInternal(name);
if ((clazz == null) && hasExternalRepositories) { clazz = super.findClass(name); } if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
|
对于 Tomcat 的类加载的 findClass 方法:
- 首先在 web 目录下查找。(重要)
- 找不到再交由父类的 findClass 来处理。
- 都找不到,那就抛出 ClassNotFoundException。
loadClass 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } clazz = findLoadedClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } ClassLoader javaseLoader = getJavaseClassLoader(); try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { } try { clazz = findClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { } try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { } } throw new ClassNotFoundException(name); }
|
总结一下加载的步骤:
- 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
- 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
- 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。
- 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。
- 加载依然失败,才使用 AppClassLoader 继续加载。
- 都没有加载成功的话,抛出异常。
总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。
- 保证了基础类不会被同时加载。
- 由保证了在同一个 Tomcat 下不同 web 之间的 class 是相互隔离的。
more
准备把有趣的代码这个系列慢慢写下去,发现编程的乐趣:
那些有趣的代码(一)–有点萌的 Tomcat 的线程池