死磕 Tomcat 系列(4)--Tomcat 中的类加载器
在学习 Tomcat 中的类加载器, 并且 Tomcat 为什么要实现自己的类加载器打破双亲委派模型原因之前, 我们首先需要知道 Java 中定义的类加载器是什么, 双亲委派模型是什么.
Java 中的类加载器
类加载器负责在程序运行时将 java 文件动态加载到 JVM 中
从 Java 虚拟机的角度来讲的话, 存在两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader): 这个类加载器是使用 C++ 语言实现的, 是虚拟机自身的一部分.
其他的类加载器: 这些类加载器都由 Java 语言实现, 独立于虚拟机外部, 并且全都继承自抽象类 java.lang.ClassLoader, 其中其他类加载器大概又分为
ExtensionClassLoader: 这个类加载器由 ExtClassLoader 实现, 它负责加载 JAVA_HOME/lib/ext 目录中的所有类, 或者被 java.ext.dir 系统变量所指定的路径中所有的类.
ApplicationClassLoader: 这个类加载器是由 AppClassLoader 实现的, 它负责加载用户类路径 (ClassPath) 上所指定的所有类, 如果应用中没有自定义自己的类加载器, 那么一般情况就是程序中默认的类加载器.
自定义加载器: 根据自己需求, 自定义加载特定路径的加载器.
image
对于任意一个类, 都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性
双亲委派模型
上图中展示的层次结构, 称之为类加载器的双亲委派模型. 双亲委派模型要求除了顶层的启动类加载器外, 其他加载器都应该有自己的父加载器. 这里的父子关系不是通过继承来实现的, 而是通过设置 parent 变量来实现的.
双亲委派模型工作过程是: 如果收到一个类加载的请求, 本身不会先加载此类, 而是会先将此请求委派给父类加载器去完成, 每个层次都是如此, 直到启动类加载器中, 只有父类都没有加载此文件, 那么子类才会尝试自己去加载.
为什么要设置双亲委派模型呢? 其实是为了保证 Java 程序的稳定运行, 例如 Object 类, 它是存放在 rt.jar 中, 无论哪一个类加载器要加载 Object 类, 最终都会委托给顶层的 BootStrapClassLoader, 所以所有的类中使用的 Object 都是同一个类, 相反如果没有双亲委派模型的话, 那么随意一个类加载器都可以定义一个新的 Object 类, 那么应用程序将会变得非常混乱. 其实双亲委派模型代码非常简单. 实现在 ClassLoader 中的 loadClass 方法下.
- 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 {
- // 如果没有父类, 则表明在顶层, 就交给 Bootstrap 类加载器加载
- c = findBootstrapClassOrNull(name);
- }
- // 如果最顶层的类也找不到, 那么就会抛出 ClassNotFoundException 异常
- } catch (ClassNotFoundException e) {
- }
- // 如果父类都没有加载过此类, 子类才开始加载此类
- if (c == null) {
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
我们可以看到 findClass 方法是需要子类自己去实现的逻辑.
Tomcat 中的类加载器
下面的简图是 Tomcat9 版本的官方文档给出的 Tomcat 的类加载器的图.
- Bootstrap
- |
- System
- |
- Common
- / \
- webapp1 Webapp2 ..
Bootstrap : 是 Java 的最高的加载器, 用 C 语言实现, 主要用来加载 JVM 启动时所需要的核心类, 例如
$JAVA_HOME/jre/lib/ext
路径下的类.
System: 会加载 CLASSPATH 系统变量所定义路径的所有的类.
Common: 会加载 Tomcat 路径下的 lib 文件下的所有类.
Webapp1,Webapp2......: 会加载 webapp 路径下项目中的所有的类. 一个项目对应一个 WebappClassLoader, 这样就实现了应用之间类的隔离了.
这 3 个部分, 在上面的 Java 双亲委派模型图中都有体现. 不过可以看到 ExtClassLoader 没有画出来, 可以理解为是跟 Bootstrap 合并了, 都是去 JAVA_HOME/jre/lib 下面加载类. 那么 Tomcat 为什么要自定义类加载器呢?
隔离不同应用: 部署在同一个 Tomcat 中的不同应用 A 和 B, 例如 A 用了 Spring2.5.B 用了 Spring3.5, 那么这两个应用如果使用的是同一个类加载器, 那么 Web 应用就会因为 jar 包覆盖而无法启动.
灵活性: Web 应用之间的类加载器相互独立, 那么就可以根据修改不同的文件重建不同的类加载器替换原来的. 从而不影响其他应用.
性能: 如果在一个 Tomcat 部署多个应用, 多个应用中都有相同的类库依赖. 那么可以把这相同的类库让 Common 类加载器进行加载.
Tomcat 自定义了 WebAppClassLoader 类加载器. 打破了双亲委派的机制, 即如果收到类加载的请求, 会尝试自己去加载, 如果找不到再交给父加载器去加载, 目的就是为了优先加载 Web 应用自己定义的类. 我们知道 ClassLoader 默认的 loadClass 方法是以双亲委派的模型进行加载类的, 那么 Tomcat 既然要打破这个规则, 就要重写 loadClass 方法, 我们可以看 WebAppClassLoader 类中重写的 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);
- }
最后借用 Tomcat 官网上的话总结:
Web 应用默认的类加载顺序是(打破了双亲委派规则):
先从 JVM 的 BootStrapClassLoader 中加载.
加载 Web 应用下 / Web-INF/classes 中的类.
加载 Web 应用下 / Web-INF/lib/*.jap 中的 jar 包中的类.
加载上面定义的 System 路径下面的类.
加载上面定义的 Common 路径下面的类.
如果在配置文件中配置了 < Loader delegate="true"/>, 那么就是遵循双亲委派规则, 加载顺序如下:
先从 JVM 的 BootStrapClassLoader 中加载.
加载上面定义的 System 路径下面的类.
加载上面定义的 Common 路径下面的类.
加载 Web 应用下 / Web-INF/classes 中的类.
加载 Web 应用下 / Web-INF/lib/*.jap 中的 jar 包中的类.
往期文章
如何断点调试 Tomcat 源码
死磕 Tomcat 系列(1)-- 整体架构
死磕 Tomcat 系列(2)--EndPoint 源码解析
死磕 Tomcat 系列(3)--Tomcat 如何做到一键式启停的
一次奇怪的 StackOverflowError 问题查找之旅
徒手撸一个简单的 RPC 框架
徒手撸一个简单的 RPC 框架(2)-- 项目改造
参考文章
http://tomcat.apache.org/tomcat-9.0-doc/class-loader-howto.html
深入理解 Java 虚拟机
深入拆解 Tomcat
https://kyfxbl.iteye.com/blog/1707237
来源: http://www.jianshu.com/p/aad78e7a1f8f