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 ...
随机推荐
- google protocol buffer——protobuf的问题及改进一
这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们完整了 ...
- Java远程连接Linux服务器并执行命令及上传文件
最近再开发中遇到需要将文件上传到Linux服务器上,至此整理代码笔记. 此种连接方法中有考虑到并发问题,在进行创建FTP连接的时候将每一个连接对象存放至 ThreadLocal<Ftp> ...
- vue init深度定制团队自己的Vue template
大家都知道,使用vue-cli可以快速的初始化一个基于Vue.js的项目,全局安装脚手架之后,你可以通过vue list命令看到官方提供的5个模板 vue list 当开发一个独立项目的时候,使用官方 ...
- 跟着兄弟连系统学习Linux-【day01】
day01-20200527 p1.unix发展历史 (1960,有一个实验室,三个团队组成,开发了Unix雏形,但是因为没有办法发版,所以就荒废了.这个小组里面有一个人,打游戏的时候 ...
- Mybatis源码学习第八天(总结)
源码学习到这里就要结束了; 来总结一下吧 Mybatis的总体架构 这次源码学习我们,学习了重点的模块,在这里我想说一句,源码的学习不是要所有的都学,一行一行的去学,这是错误的,我们只需要学习核心,专 ...
- 在一张id连续的表中找出缺失的id
有这样一张表: create table tb_lostid( id number(6,0) primary key not null, name nvarchar2(20) not null ) 可 ...
- pwnable.kr之bof
打开题目: 先下载题目给我们的两个文件,查看文件信息: 发现没有执行的权限,所以先增加文件bof的执行权限,执行: 没发现啥,然后查看代码, #include <stdio.h> #inc ...
- Java中枚举的用法
public enum Week { DAY1("周一", 0.9), DAY2("周二", 0.9), DAY3("周三", 0.8), ...
- String源码浅析
如果问你,开发过程中用的最多的类是哪个?你可能回答是HashMap,一个原因就是HashMap的使用量的确很多,还有就是HashMap的内容在面试中经常被问起. 但是在开发过程中使用最多的类其实并不是 ...
- SpringCloud-config分布式配置
为什么要统一管理微服务配置? 随着微服务不断的增多,每个微服务都有自己对应的配置文件.在研发过程中有测试环境.UAT环境.生产环境,因此每个微服务又对应至少三个不同环境的配置文件.这么多的配置文件,如 ...