Tomcat源码分析-类加载器

news/2024/4/26 6:13:34/文章来源:https://blog.csdn.net/weixin_37948564/article/details/129208023

类加载器

在分析 tomcat 类加载之前,我们简单的回顾下 java 体系的类加载器

  • 启动类加载器(Bootstrap ClassLoader):加载对象是java的核心类库,把一些的 java 类加载到 jvm 中,它并不是我们熟悉的 ClassLoader,而是 jvm 层面由 C/C++ 实现的类加载器,负责加载 $JAVA_HOME/jre/lib 目录下 jvm 指定的类库,它是无法被 java 应用程序直接使用的
  • 扩展类加载器(Extension Classloader):它是一个 ClassLoader 实例,父加载器是启动类加载器,它负责加载 $JAVA_HOME/jre/lib/ext 目录的类库
  • 应用类加载器(Application ClassLoader):又叫做系统类加载器(System ClassLoader),负责加载用户类路径(-cp参数)指定的类库,可以通过 ClassLoader.getSystemClassLoader() 获取,它也是由启动类加载器加载的
  • 自定义类加载器:应用程序根据自己的需求开发的类加载器,可以继承 ClassLoader,当然也可以不继承

下图描述了类加载器的关系图,其中自定义类加载器有N多个

我们知道 java.lang.ClassLoader 有双亲委派机制(准确的说是单亲,因为只有一个parent),这只是 java 建议的规范,我们也可以不遵循这条规则,但是建议遵循该规则。此外,有一点需要注意的是,类加载器不局限于 ClassLoader,我们也可以自己实现一个类加载器,只要你加载出来的 Class 符合 jvm 规范即可

我们在日常开发工作中,经常会遇到类冲突的情况,明明 classpath 下面的类有这个方法,但是一旦跑线上环境就出错,比如NoSuchMethodErrorNoClassDefFoundErrorNoClassDefFoundError 等。我们可以使用 jvm 参数 -verbose:class 方便地定位该问题,使用该参数可以快速地定位某个类是从哪个jar包加载的,而不是一味地埋头苦干,求百度,找Google。下面是使用 -verbose:class jvm 参数的部分日志输出

[Loaded org.springframework.context.annotation.CommonAnnotationBeanPostProcessor from file:/D:/tomcat/webapps/touch/WEB-INF/lib/spring-context-4.3.7.RELEASE.jar]
[Loaded com.alibaba.dubbo.rpc.InvokerListener from file:/D:/tomcat/webapps/touch/WEB-INF/lib/dubbo-2.5.3.jar]

我们有必要了解下关于类加载有几个重要的知识点:

  • 在 Java 中我们用完全类名来标识一个类,而在 JVM 层面,使用完全类名 + CloassLoader 对象实例 ID 作为唯一标识,因此使用不同实例的类加载器,加载的两个同名的类,他们的类实例是不同的,并且不能强制转换
  • 在双亲委派机制中,类加载器查找类时,是一层层往父类加载器查找的,最后才查看自己,如果都找不到则会抛出异常,而不是一层层往下找的
  • 每个运行中的线程都有一个 CloassLoader,并且会从父线程中继承(默认是应用类加载器),在没有显式声明由哪个类加载器加载类时(比如 new 关键字),将默认由当前线程的类加载器加载该类

由于篇幅有限,关于类加载的过程这里不再展开了,可以参考厮大的博客

  • Java虚拟机类加载机制:Java虚拟机类加载机制_朱小厮的博客-CSDN博客_加载、验证、准备、初始化、和卸载这5个阶段的顺序是确定的,类的加载

tomcat 类加载器

根据实际的应用场景,我们来分析下 tomcat 类加载器需要解决的几个问题

  • 为了避免类冲突,每个 webapp 项目中各自使用的类库要有隔离机制
  • 不同 webapp 项目支持共享某些类库
  • 类加载器应该支持热插拔功能,比如对 jsp 的支持、webapp 的 reload 操作

为了解决以上问题,tomcat设计了一套类加载器,如下图所示。在 Tomcat 里面最重要的是 Common 类加载器,它的父加载器是应用程序类加载器,负责加载 ${catalina.base}/lib${catalina.home}/lib 目录下面所有的 .jar 文件和 .class 文件。下图的虚线部分,有 catalina 类加载器、share 类加载器,并且它们的 parent 是 common 类加载器,默认情况下被赋值为 Common 类加载器实例,即 Common 类加载器、catalina 类加载器、 share 类加载器都属于同一个实例。当然,如果我们通过修改 catalina.properties 文件的 server.loader 和 shared.loader 配置,从而指定其创建不同的类加载器

我们先从 Bootstrap 这个入口说起,在执行 init 的时候会实例化类加载器,在初始化类加载器之后立即设置线程上下文类加载器(Thread Context ClassLoader)为 catalina 类加载器,接下来是为 Catalina 组件指定父类加载器。为什么要设置线程上下文的类加载器呢?一方面,很多诸如 ClassUtils 之类的编码,他们在获取 ClassLoader 的时候,都是先尝试从 Thread 上下文中获取 ClassLoader,例如:ClassLoader cl = Thread.currentThread().getContextClassLoader(); 另一方面,在没有显式指定类加载器的情况下,默认使用线程的上下文类加载器加载类,由于 tomcat 的大部分 jar 包都在 ${catalina.hom}/lib 目录,因此需要将线程类加载器指定为 catalina 类加载器,否则加载不了相关的类。

双亲委派模型存在设计上的缺陷,在某些应用场景下,例如加载 SPI 实现(JNDI、JDBC等),如果我们严格遵循双亲委派的一般性原则,使用应用程序类加载器,由于这些 SPI 实现在厂商的 jar 包中,所以应用程序类加载器不可能认识这些代码啊,怎么办?为了解决这个问题,Java 设计团队引入了一个不太优雅的设计:Thread Context ClassLoader,有了这个线程上下文类加载器,我们便可以做一些“舞弊”的事情了,JNDI 服务可以使用这个类加载器加载 SPI 需要的代码,JDBC、JAXB 也是如此。这样,双亲委派模型便被破坏了。

Bootstrap.java
public void init() throws Exception {// 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再看initClassLoaders();// 设置上下文类加载器为 catalinaLoader Thread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);// 反射方法实例化Catalina,后面初始化Catalina用了很多反射,不知道意图是什么Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");Object startupInstance = startupClass.getConstructor().newInstance();//TODO 为Catalina对象设置其父加载器为shared类加载器,默认情况下就是catalina类加载器// 引用Catalina实例catalinaDaemon = startupInstance;

catalina.properties 文件的相关配置如下所示

common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=

我们再来看下创建类加载器的代码,首先是创建 common 类加载器,从 catalina.properties 中读取 common.loader 配置作为 common 类加载器的路径。我们注意到 common.loader 中存在 ${catalina.base}${catalina.home} 这样的占位符,在读取配置之后,tomcat 会进行替换处理,同理 server.loadershared.loader 也可以使用这样的占位符,或者系统变量作为占位符,有兴趣的童鞋可以参考下 Bootstrap.replace(String str) 源码,如果在项目中有相同的场景的话,可以直接 copy 该代码。

Bootstrap.javaprivate void initClassLoaders() {try {// 从catalina.properties中读取common.loader配置作为common类加载的路径commonLoader = createClassLoader("common", null);if( commonLoader == null ) {commonLoader=this.getClass().getClassLoader();}// 如果未指定server.loader和shared.loader配置,则catalina和shared类加载器都是common类加载器catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}

接下来,我们再来看下 tomcat 是如何创建 common 类加载器的。关键代码如下所示,在创建类加载器时,会读取相关的路径配置,并把路径封装成 Repository 对象,然后交给 ClassLoaderFactory 创建类加载器。

Bootstrap.java
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {// 从catalina.propeties中读取配置,并替换 catalina.home、或者catalina.base,或者环境变量String value = CatalinaProperties.getProperty(name + ".loader");value = replace(value);// 遍历目录,并对路径进行处理List<Repository> repositories = new ArrayList<>();String[] repositoryPaths = getPaths(value);for (String repository : repositoryPaths) {//TODO 将路径封装成 Repository 对象}return ClassLoaderFactory.createClassLoader(repositories, parent);

我们再进一步对 ClassLoaderFactory 进行分析,都是细节上的处理,比如利用文件路径构造带有明显协议的 URL 对象,例如本地文件的标准 URL 是 file:/D:/app.jar。另外,在创建 URLClassLoader 的时候还需要考虑 jdk 对权限控制的影响,因此 tomcat 利用 AccessController 创建 URLClassLoader,由此可见 tomcat 编码的严谨性。而我们在实际的开发过程中,有时候需要自定义类加载器,但往往不会考虑权限控制这块,所以在对类加载器进行编码时需要注意一下

ClassLoaderFactory.java
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent)throws Exception {Set<URL> set = new LinkedHashSet<>();if (repositories != null) {for (Repository repository : repositories)  {// 对不同类型的 Repository 对象进行处理,将路径转换为URL类型// 因为 URL 类型带有明显的协议,比如jar:xxx、file:xxx}}// 将对应的路径组装成 URLfinal URL[] array = set.toArray(new URL[set.size()]);// 在创建 URLClassLoader 需要考虑到 AccessController 的影响return AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {public URLClassLoader run() {if (parent == null) return new URLClassLoader(array);else return new URLClassLoader(array, parent);}});
}private static URL buildClassLoaderUrl(File file) throws MalformedURLException {String fileUrlString = file.toURI().toString();fileUrlString = fileUrlString.replaceAll("!/", "%21/"); // 转换成URL编码return new URL(fileUrlString);

OK,前面介绍了 tomcat 创建类加载器的过程,接下来我们看下 tomcat 类加载器的具体应用场景

WebappClassLoader

在前面,我们介绍了 tomcat 类加载器的设计,每个 webapp 使用单独的类加载器完成我们开发的 webapp 应用程序的类加载,而每一个 webapp 对应一个 WebappClassLoader。tomcat7 默认使用 WebappClassLoader 类加载器,而 tomcat8 默认使用 ParallelWebappClassLoader,支持并行加载类的特性,这也算是 tomcat8 做的一些优化吧,而实际上也是利用 jdk 的功能,需要同时满足以下两点才支持并行加载类,并且一旦注册了并行加载的能力,就不能回退了

1、 没有创建调用者的实例
2、 调用者的所有超类(除了类对象)都是并行注册的

基于上面两点,因此,ParallelWebappClassLoader 在 static 代码块中注册并行加载机制,而它的父类 URLClassLoader 父类也是具有并行能力的,关键代码如下所示:

public class ParallelWebappClassLoader extends WebappClassLoaderBase {static {boolean result = ClassLoader.registerAsParallelCapable();if (!result) {log.warn(sm.getString("webappClassLoaderParallel.registrationFailed"));}}// 省略无关代码...

WebappClassLoader 的类图如下所示,其中 WebappClassLoaderBase 实现了主要的逻辑,并且继承了 Lifecycle,在 tomcat 组件启动、关闭时会完成资源的加载、卸载操作,例如在 start 过程会读取我们熟悉的 /WEB-INF/classes/WEB-INF/lib 资源,并且记录每个 jar 包的时间戳方便重载 jar 包;而在组件 stop 的时候,会清理已经加载的资源;destory 时会显式地触发 URLClassLoader.close()。这个 Lifecycle 真是无处不在啊

单独的类加载器是无法获取 webapp 的资源信息的,因此 tomcat 引入了 WebappLoader,便于访问 Context 组件的信息,同时为 Context 提供类加载的能力支持,下面我们分析下 WebappLoader 的底层实现

WebappLoader

我们先来看看 WebappLoader 几个重要的属性,内部持有 Context 组件,并且有个我们熟悉的 reloadable 参数,如果设为 true,则会开启类的热加载机制

public class WebappLoader extends LifecycleMBeanBaseimplements Loader, PropertyChangeListener {private WebappClassLoaderBase classLoader = null;   // 默认使用ParallelWebappClassLoaderprivate Context context = null;private String loaderClass = ParallelWebappClassLoader.class.getName();private ClassLoader parentClassLoader = null;       // 父加载器,默认为 catalina 类加载器private boolean reloadable = false;                 // 是否支持热加载类private String classpath = null;

在 tomcat 中,每个 webapp 对应一个 StandardContext,在 start 过程便会实例化 WebappLoader,并且调用其 start 方法完成初始化,包括创建 ParallelWebappClassLoader 实例,然后,还会启动 Context 的子容器。注意,这两个过程,都会将线程上下文类加载器指定为 ParallelWebappClassLoader 类加载器,在完成 webapp 相关的类加载之后,又将线程上下文类加载器设置为 catalina 类加载器。Context 容器的启动过程,这里便不再重复了,感兴趣的童鞋请查看前面的博文 webapp源码分析

StandardContext.javaprotected synchronized void startInternal() throws LifecycleException {// 实例化 Loader 实例,它是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader(getParentClassLoader());webappLoader.setDelegate(getDelegate());setLoader(webappLoader);    // 使用了读写锁控制并发问题}// 将 Loader 中的 ParallelWebappClassLoader 绑定到当前线程中,并返回 catalian 类加载器ClassLoader oldCCL = bindThread();try {if (ok) {// 如果 Loader 是 Lifecycle 实现类,则启动该 LoaderLoader loader = getLoader();if (loader instanceof Lifecycle) {((Lifecycle) loader).start();}// 设置 ClassLoader 的各种属性setClassLoaderProperty("clearReferencesRmiTargets", getClearReferencesRmiTargets());// 省略……// 解除线程上下文类加载器绑定unbindThread(oldCCL);oldCCL = bindThread();// 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是加载 Context 的子容器fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);// 启动子容器for (Container child : findChildren()) {if (!child.getState().isAvailable()) {child.start();}}}} finally {// Unbinding threadunbindThread(oldCCL);}

而 WebappLoader 在 stop 的时候,会销毁 WebappClassLoader,并且进行回收,促使 jvm 卸载已加载的类

WebappLoader.java@Override
protected void stopInternal() throws LifecycleException {// 省略不相关代码...if (classLoader != null) {try {classLoader.stop();} finally {classLoader.destroy();}}classLoader = null; // help gc

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_73778.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数影周报:动视暴雪疑似数据泄露,数据出境安全评估申报最新进展

本周看点&#xff1a;动视暴雪疑似员工敏感信息及游戏数据泄露&#xff1b;谷歌云计算部门&#xff1a;两名员工合用一个工位&#xff1b;数据出境安全评估申报最新进展&#xff1b;TikTok Shop东南亚商城在泰国和菲律宾公布&#xff1b;智己汽车获九大金融机构50亿元贷款签约.…

Redis:实现全局唯一ID

Redis&#xff1a;实现全局唯一ID一. 概述二. 实现&#xff08;1&#xff09;获取初始时间戳&#xff08;2&#xff09;生成全局ID三. 测试为什么可以实现全局唯一&#xff1f;其他唯一ID策略补充&#xff1a;countDownLatch一. 概述 全局ID生成器&#xff1a;是一种在【分布式…

RK3568平台开发系列讲解(驱动基础篇)中断子系统框架

🚀返回专栏总目录 文章目录 一、中断硬件的组成二、软件框架三、中断常见概念沉淀、分享、成长,让自己和他人都能有所收获!😄 📢中断是指 CPU 正常运行期间,由于内外部事件或程序预先安排的事件,引起的 CPU 暂时停止正在运行的程序, 转而为该内部或外部预先安排的事…

基于Frenet优化轨迹的⾃动驾驶动作规划⽅法

动作规划&#xff08;Motion Control&#xff09;在⾃动驾驶汽⻋规划模块的最底层&#xff0c;它负责根据当前配置和⽬标配置⽣成⼀序列的动作&#xff0c;本⽂介绍⼀种基于Frenet坐标系的优化轨迹动作规划⽅法&#xff0c;该⽅法在⾼速情况下的ACC辅助驾驶和⽆⼈驾驶都具有较强…

2023年,尽量还是别裸辞了吧···

你知道什么叫 度日如年 吗&#xff1f;就是在家待业的每一天。你知道什么叫心焦如焚吗&#xff1f;就是投出100份简历却等不来一个回应。 当前就业环境&#xff0c;裁员、失业消息满天飞&#xff0c;好像能有一份工作就不错了&#xff0c;更别说高薪。其实这只是一方面。另一方…

基于BP神经网络的性别识别,BP神经网络详细原理,自编码神经网络代码,神经网络案例之18

目标 背影 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数&#xff0c; BP神经网络的传递函数 数据 神经网络参数 基于BP神经网络 性别识别的MATLAB代码 效果图 结果分析 展望 背影 男人体内蛋白质比例大&#xff0c;女生…

网易的“草长莺飞二月天”:增长稳健,加码研发,逐浪AI

2月23日&#xff0c;网易发布了2022年第四季度财报。 这是网易与暴雪分道扬镳后的首份财报&#xff0c;加上近期AIGC热度扩散至游戏、教育等各个领域&#xff0c;网易第四季度业绩及其对于GPT等热门技术的探索受到市场关注。 根据财报&#xff0c;第四季度&#xff0c;网易营…

SAFe(Scaled Agile Framework)学习笔记

1.SAFe 概述 SAFe&#xff08;Scaled Agile Framework&#xff09;是一种面向大型企业的敏捷开发框架&#xff0c;旨在协调多个团队和部门的协同工作&#xff0c;以实现高效的软件开发和交付。下面是SAFe框架的简单介绍总结&#xff1a; SAFe框架包括以下四个层次&#xff1a…

【LVGL】学习笔记--(1)Keil中嵌入式系统移植LVGL

一 LVGL简介最近emwin用的比较烦躁&#xff0c;同时被LVGL酷炫的界面吸引到了&#xff0c;所以准备换用LVGL试试水。LVGL(轻量级和通用图形库)是一个免费和开源的图形库&#xff0c;它提供了创建嵌入式GUI所需的一切&#xff0c;具有易于使用的图形元素&#xff0c;美丽的视觉效…

Unable to connect to Redis无法连接到Redis

文章目录项目场景&#xff1a;问题描述原因分析&#xff1a;解决方案&#xff1a;项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 在某个项目中的提交按钮不好用 org.springframework.data.redis.RedisConnectionFailureException: Unable to con…

程序员必备的软技能-金字塔原理拆解(上)

原书 290千字&#xff0c;本文预计 14千字&#xff0c;拆解比 20&#xff1a;1&#xff0c;预计阅读时长 15分钟序言日常工作中&#xff0c;常常因为思维、表达方式不对产生不想要的结果&#xff1a;写了一个小时的周报&#xff0c;领导却不满意&#xff1f;跟团队讲了半天自己…

go module构建项目

在go 1.11版本中引入了Go Module内置的包管理模块&#xff0c;是GOPATH的替代品&#xff0c;集成了版本控制和软件包分发支持的功能。即go使用modules管理依赖&#xff0c;项目依赖构建时不需要再依赖GOPATH环境变量。 要使用go module首先要激活modules .升级go到1.11版本 .这…

Mac电脑_GitHub提交项目至仓库

第一步&#xff08;准备工作&#xff09;&#xff1a; Mac 电脑自带 git &#xff0c; 无需安装 1. 创建一个项目 demo1 在 github 上 2. 创建 ssh 密钥 打开终端&#xff1a; ssh-keygen -t rsa -C "your_emailyouremail.com" 此处输入两次密码&#xff0c; 直接…

MyBatis-常用SQL操作

一、动态SQL 1.概述】 1.1动态SQL&#xff1a; 是 MyBatis 的强大特性之一&#xff0c;解决拼接动态SQL时候的难题&#xff0c;提高开发效 1.2分类&#xff1a; if choose(when,otherwise) trim(where,set) foreach 2.if 2.1 做 where 语句后面条件查询的,if 语句是可以…

【Java基础 下】 027 -- 异常、File、综合案例

目录 一、异常 1、异常的分类 ①、Error ②、Exception ③、小结 2、编译时异常和运行时异常 ①、编译时异常 ②、运行时异常 ③、为什么异常要分成编译时异常和运行时异常&#xff1f; ④、小结&#xff08;运行时异常和编译时异常的区别&#xff09; 3、异常的作用 ①、查看b…

WindowsPowerShell 停止、启动、暂停和重启服务、卸载服务

PowerShell 停止、启动、暂停和重启服务、卸载服务 PowerShell 停止、启动、暂停和重启服务 官文 powershell卸载服务 官文 目录PowerShell 停止、启动、暂停和重启服务、卸载服务停止、启动、暂停和重启停止服务启动服务暂停服务重启服务卸载移除服务停止、启动、暂停、重启…

4EVERLAND:ERC-721 Token的存储选择

4EVERLAND&#xff1a;一个 Web3 基础设施&#xff0c;可促进项目更轻松、更快速地托管前端、存储数据/NFT/文件&#xff0c;并在 IPFS、Arweave 和 Dfinity 之上访问它们。 NFT , 数字所有权 使用以太坊标准的 NFT 创新ERC-721解决了互联网内容的主要问题之一&#xff1a;所…

想成为一名专业黑客,但不知道从哪里学起?我来教你。

成为一名黑客需要学什么&#xff1f; 想成为一名专业黑客&#xff0c;但不知道从哪里学起”很多人在后台问过这个问题&#xff0c;今天就为你介绍成为专业黑客必须学习的十个方面的知识&#xff0c;希望能为迷惘中的你指明方向。 想要成为网络hacker黑客&#xff1f;先来学习…

测试员拿到新项目怎么着手测试?不要慌,照做准没错

一、目标 结合公司现有的项目情况制定合理规范的测试流程&#xff0c;提高测试效率和产品质量&#xff0c;尽可能减少客户对产品的问题反馈&#xff0c; 核心还是要加强项目组成员之间的工作交流和沟通&#xff0c;保证整个项目的高效率的按质按量的交付。 二、测试流程说明…

【Unity VR开发】结合VRTK4.0:创建物理按钮

语录&#xff1a; 如今我努力奔跑&#xff0c;不过是为了追上那个曾经被寄予厚望的自己 前言&#xff1a; 使用线性关节驱动器和碰撞体从动器可以轻松创建基于物理的按钮&#xff0c;以使交互者能够在物理上按下按钮控件&#xff0c;然后挂钩到驱动器事件中以了解按钮何时被按…