Tomcat 第六篇:类加载机制

1. 引言
Tomcat 在部署 Web 应用的时候,是将应用放在 webapps 文件夹目录下,而 webapps 对应到 Tomcat 中是容器 Host ,里面的文件夹则是对应到 Context ,在 Tomcat 启动以后, webapps 中的所有的 Web 应用都可以提供服务。
这里会涉及到一个问题, webapps 下面不止会有一个应用,比如有 APP1 和 APP2 两个应用,它们分别有自己独立的依赖 jar 包,这些 jar 包会位于 APP 的 WEB-INFO/lib 这个目录下,这些 jar 包大概率是会有重复的,比如常用的 Spring 全家桶,在这里面,版本肯定会有不同,那么 Tomcat 是如何处理的?
2. JVM 类加载机制
说到 Tomcat 的类加载机制,有一个绕不开的话题是 JVM 是如何进行类加载的,毕竟 Tomcat 也是运行在 JVM 上的。
以下内容参考自周志明老师的 「深入理解 Java 虚拟机」。
2.1 什么是类的加载
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象, Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被 「首次主动使用」 时再加载它, JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误( LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件
2.2 类生命周期
接下来,我们看下一个类的生命周期:
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

2.3 双亲委派模型
Java 提供三种类型的系统类加载器:
- 启动类加载器(Bootstrap ClassLoader):由 C++ 语言实现,属于 JVM 的一部分,其作用是加载
<JAVA_HOME>\lib目录中的文件,或者被-Xbootclasspath参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar ),而不是该目录下所有的文件。启动类加载器无法被 Java 程序直接引用。 - 扩展类加载器( Extension ClassLoader ):由
sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 - 应用程序类加载器( Application ClassLoader ):也称系统类加载器,由
sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径( Class Path )上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型的工作机制:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么?
例如类 java.lang.Object ,它存放在 rt.jar 之中。无论哪一个类加载器都要加载这个类。最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载。因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户编写了一个称为 「java.lang.Object」 的类,并存放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类, java 类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。
3. Tomcat 类加载机制
先整体看下 Tomcat 类加载器:

可以看到,在原来的 JVM 的类加载机制上面, Tomcat 新增了几个类加载器,包括 3 个基础类加载器和每个 Web 应用的类加载器。
3 个基础类加载器在 conf/catalina.properties 中进行配置:
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
- Common: 以应用类加载器为父类,是 Tomcat 顶层的公用类加载器,其路径由
conf/catalina.properties中的common.loader指定,默认指向${catalina.home}/lib下的包。 - Catalina: 以 Common 类加载器为父类,是用于加载 Tomcat 应用服务器的类加载器,其路径由
server.loader指定,默认为空,此时 Tomcat 使用 Common 类加载器加载应用服务器。 - Shared: 以 Common 类加载器为父类,是所有 Web 应用的父类加载器,其路径由
shared.loader指定,默认为空,此时 Tomcat 使用 Common 类加载器作为 Web 应用的父加载器。 - Web 应用: 以 Shared 类加载器为父类,加载
/WEB-INF/classes目录下的未压缩的 Class 和资源文件以及/WEB-INF/lib目录下的 jar 包,该类加载器只对当前 Web 应用可见,对其他 Web 应用均不可见。
4. Tomcat 类加载机制源码
4.1 ClassLoader 的创建
先看下加载器类图:

先从 BootStrap 的 main 方法看起:
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
// 省略其余代码...
}
}
可以看到这里先判断了 bootstrap 是否为 null ,如果不为 null 直接把 Catalina ClassLoader 设置到了当前线程,如果为 null 下面是走到了 init() 方法。
public void init() throws Exception {
// 初始化类加载器
initClassLoaders();
// 设置线程类加载器,将容器的加载器传入
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 设置区安全类加载器
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 省略其余代码...
}
接着这里看到了会调用 initClassLoaders() 方法进行类加载器的初始化,初始化完成后,同样会设置 Catalina ClassLoader 到当前线程。
private void initClassLoaders() {
try {
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();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
看到这里应该就清楚了,会创建三个 ClassLoader : CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好对应前面介绍的三个基础类加载器。
接着进入 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<>();
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));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
可以看到,这里加载的资源正好是我们刚才看到的配置文件 conf/catalina.properties 中的 common.loader , server.loader 和 shared.loader 。
4.2 ClassLoader 加载过程
直接打开 ParallelWebappClassLoader ,至于为啥不是看 WebappClassLoader ,从名字上就知道 ParallelWebappClassLoader 是一个并行的 WebappClassLoader 。
然后看下 ParallelWebappClassLoader 的 loadclass 方法是在它的父类 WebappClassLoaderBase 中实现的。
4.2.1 第一步:
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
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 省略其余...
首先调用 findLoaderClass0() 方法检查 WebappClassLoader 中是否加载过此类。
protected Class<?> findLoadedClass0(String name) {
String path = binaryNameToPath(name, true);
ResourceEntry entry = resourceEntries.get(path);
if (entry != null) {
return entry.loadedClass;
}
return null;
}
WebappClassLoader 加载过的类都存放在 resourceEntries 缓存中。
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();
4.2.2 第二步:
// 省略其余...
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 省略其余...
如果第一步没有找到,则继续检查 JVM 虚拟机中是否加载过该类。调用 ClassLoader 的 findLoadedClass() 方法检查。
4.2.3 第三步:
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) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
如果前两步都没有找到,则使用系统类加载该类(也就是当前 JVM 的 ClassPath )。为了防止覆盖基础类实现,这里会判断 class 是不是 JVMSE 中的基础类库中类。
4.2.4 第四步:
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
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
}
}
先判断是否设置了 delegate 属性,设置为 true ,那么就会完全按照 JVM 的"双亲委托"机制流程加载类。
若是默认的话,是先使用 WebappClassLoader 自己处理加载类的。当然,若是委托了,使用双亲委托亦没有加载到 class 实例,那还是最后使用 WebappClassLoader 加载。
4.2.5 第五步:
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
}
若是没有委托,则默认会首次使用 WebappClassLoader 来加载类。通过自定义 findClass() 定义处理类加载规则。
findClass() 会去 Web-INF/classes 目录下查找类。
4.2.6 第六步:
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
}
}
若是 WebappClassLoader 在 /WEB-INF/classes 、 /WEB-INF/lib 下还是查找不到 class ,那么无条件强制委托给 System 、 Common 类加载器去查找该类。
4.2.7 小结
Web 应用类加载器默认的加载顺序是:
- 先从缓存中加载;
- 如果没有,则从 JVM 的 Bootstrap 类加载器加载;
- 如果没有,则从当前类加载器加载(按照 WEB-INF/classes 、 WEB-INF/lib 的顺序);
- 如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序是 AppClassLoader 、 Common 、 Shared 。
参考
https://www.jianshu.com/p/69c4526b843d
Tomcat 第六篇:类加载机制的更多相关文章
- 【JVM第二篇--类加载机制】类加载器与双亲委派模型
写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.什么是类加载器 在类加载过程中,加载阶段有一个动作是"通过一个类的全限 ...
- 【JVM第一篇--类加载机制】类加载过程
写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.什么是类加载过程 (1).概述 我们编写的类(.java文件)会被编译器(如ja ...
- Tomcat系列(7)——Tomcat类加载机制
1. 核心部分 1. 类加载器: 通过一个类的全限定名来获取描述此类的二进制字节流. 对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一 ...
- 学习Tomcat(六)之类加载器
通过前面的文章我们知道,Tomcat的请求最终都会交给用户配置的servlet实例来处理.Servlet类是配置在配置文件中的,这就需要类加载器对Servlet类进行加载.Tomcat容器自定义了类加 ...
- 简单了解Tomcat与OSGi的类加载器架构
前言: 本次博客主要是对Tomcat与OSGi的类加载器架构,所以就需要对tomcat.OSGi以及类加载机制有所了解 类加载可以在http://www.cnblogs.com/ghoster/p/7 ...
- 谈谈 Java 类加载机制
概述 类加载器主要分为两类,一类是 JDK 默认提供的,一类是用户自定义的. JDK 默认提供三种类加载器: Bootstrap ClassLoader 启动类加载器:每次执行 java 命令时都会使 ...
- 【JVM第六篇--对象】对象的实例化、内存布局和访问定位
写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.对象的实例化 在平常写代码的过程中,我们用class关键字定义的类只是一个类的模 ...
- 图解Tomcat类加载机制
说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷. 之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,与我们自己的web项目整合后无法直接断点调试.后来同 ...
- 《转载》图解Tomcat类加载机制
本文转载自http://www.cnblogs.com/xing901022/p/4574961.html 说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷. 之前实习的时候学习j ...
随机推荐
- Python技术调查
1. IDE 2. Local Debugging & Remote Debugging 3. Profiling
- 【HttpRunner v3.x】笔记—8.运行testcase的几种方式
在之前的demo过程中,已经运行过testcase了,那这篇就也来汇总一下,运行case相关的知识点. 一.运行testcase的几种场景 1. 运行单个case 通常单个case的话我会在编辑器里用 ...
- 集成react-native-image-picker时,报错Couldn't get file path for photo
1. 版本环境: "react": "16.13.1", "react-native": "0.63.2", " ...
- Tomact的中文乱码设置
在使用Tomact时,有时候使用中文时,窗口会把中文部分显示为乱码,这时需要修改相关配置,让其正常显示. 1.修改server.xml的配置,解决显示窗口的乱码 打开Tomcat下/bin/serve ...
- FZU - 2037 -Maximum Value Problem(规律题)
Let’s start with a very classical problem. Given an array a[1…n] of positive numbers, if the value o ...
- 平衡二叉搜索树/AVL二叉树 C实现
//AVTree.h #ifndef MY_AVLTREE_H #define MY_AVLTREE_H typedef int ElementType; struct TreeNode { Elem ...
- List集合对象去重及按属性去重的8种方法-java基础总结系列第六篇
最近在写一些关于java基础的文章,但是我又不想按照教科书的方式去写知识点的文章,因为意义不大.基础知识太多了,如何将这些知识归纳总结,总结出优缺点或者是使用场景才是对知识的升华.所以我更想把java ...
- Spring IoC 到底是什么
前言 「上一篇文章」我们对 Spring 有了初步的认识,而 Spring 全家桶中几乎所有组件都是依赖于 IoC 的. 刚开始听到 IoC,会觉得特别高大上,但其实掰开了很简单. 跟着我的脚步,一文 ...
- linux:apache-配置基于域名的虚拟机主机
一个http服务要配置多个站点,就需要用到虚拟机主机. 虚拟机主机一般有三类:1.基于域名 2.基于端口 3.基于ip 举例操作基于域名配置三个站点: 域名 站点目录 www.eejind.com ...
- Appium自动化Android环境搭建
前言: 本系列教程用于个人经验记录,用于他人借鉴,提供一定参考价值.经常会有一种感觉,工具或技术在某一阶段使用比较熟练,过一段时间就可能会遗忘,俗话说好记性不如烂笔头,以此记录. appium简介 ...