八.吊打面试官系列-Tomcat优化-深入源码剖析Tomcat如何打破双亲委派

前言

上篇文章《Tomcat优化-深入Tomcat底层原理》我们从宏观上分析了一下Tomcat的顶层架构以及核心组件的执行流程。本篇文章我们从源码角度来分析Tomcat的类加载机制,且看它是如何打破JVM的ClassLoader双亲委派的

Tomcat ClassLoader 初始化

Tomcat的启动类是在 org.apache.catalina.startup.Bootstrap#main中,通过执行main方法来启动,该方法中会创建一个Bootstrap对象,然后执行Bootstrap.init()方法来进行初始化。同时该方法中维护了 Bootstrap 的 start ,stop等生命周期方法的入口,源码如下

public static void main(String args[]) {synchronized (daemonLock) {if (daemon == null) {// Don't set daemon until init() has completedBootstrap bootstrap = new Bootstrap();try {//1.初始化Tomcatbootstrap.init();} catch (Throwable t) {handleThrowable(t);log.error("Init exception", t);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);}}try {String command = "start";if (args.length > 0) {command = args[args.length - 1];}//触发startd指令if (command.equals("startd")) {args[args.length - 1] = "start";daemon.load(args);daemon.start();//触发 stop执行} else if (command.equals("stopd")) {args[args.length - 1] = "stop";daemon.stop();} else if (command.equals("start")) {daemon.setAwait(true);daemon.load(args);daemon.start();if (null == daemon.getServer()) {System.exit(1);}} else if (command.equals("stop")) {daemon.stopServer(args);} else if (command.equals("configtest")) {daemon.load(args);if (null == daemon.getServer()) {System.exit(1);}System.exit(0);} else {log.warn("Bootstrap: command \"" + command + "\" does not exist.");}} catch (Throwable t) {// Unwrap the Exception for clearer error reportingif (t instanceof InvocationTargetException && t.getCause() != null) {t = t.getCause();}handleThrowable(t);log.error("Error running command", t);System.exit(1);}}

下面我们切入到bootstrap#init初始化方法中,该方法中会调用 initClassLoaders初始化Tomcat自定义的类加载器,下面我们可以看到三个类加载器分别是:commonLoader,catalinaLoader,sharedLoader。三个类加载器创建好之后,会通过catalinaLoader加载 Catalina.class并实例化它。并把sharedLoader作为Catalina的setParentClassLoader父类加载器。如下:


//Tomcat中自定义的classLoader
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;//初始化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);}}//初始化bootstrap
public void init() throws Exception {//初始化类加载器initClassLoaders();Thread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);// Load our startup class and call its process() methodif (log.isTraceEnabled()) {log.trace("Loading startup class");}//加载 Catalina 类Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");//实例化 Catalina 对象Object startupInstance = startupClass.getConstructor().newInstance();// Set the shared extensions class loaderif (log.isTraceEnabled()) {log.trace("Setting startup class properties");}String methodName = "setParentClassLoader";Class<?> paramTypes[] = new Class[1];paramTypes[0] = Class.forName("java.lang.ClassLoader");Object paramValues[] = new Object[1];paramValues[0] = sharedLoader;//调用 Catalina的setParentClassLoader,为Catalina设置 parent 类加载器Method method = startupInstance.getClass().getMethod(methodName, paramTypes);method.invoke(startupInstance, paramValues);catalinaDaemon = startupInstance;}

JVM ClassLoader 双亲委派

这里看起来会有些懵逼,如果要理解Tomcat的类加载机制就要先理解JVM的类加载机制。下面是JVM的类加载器

在这里插入图片描述
在JVM中分为启动类加载器,扩展类加载器,应用程序类加载器,和自定义加载器4类,他们分别加载

  • 启动类加载器:加载 jre/lib 目录下的jar包,其中包括了java的基本环境,比如:java.lang,java.io 等包下的基础类
  • 扩展类加载器:加载 jre/lib/ext 目录下的jar包,也是java自带的一些基础包
  • 应用程序类加载器:加载classpath下的代码,也就是我们自己的代码,以及pom中导入的jar
  • 自定义加载器:程序员自己定义的类加载器,按照程序员指定的需求进行加载

JVM的这些类加载器遵循双亲委派设计模式进行类的加载,大概的含义是子加载器优先委派父加载器进行加载父加载器没有加载子加载器才加载比如:AppClassLoader加载之前会先调用父加载器ExtClassLoader的加载方法,而ExtClassLoader加载之前会调用BootstrapClassLoader方法优先进行加载,也就形成了加载顺序其实是从上往下进行加载,如果父加载器加载了某个类,子加载器将不再会加载。在Jvm中提供了一个类加载器的顶层类java.lang.ClassLoader ,所有的类加载器都是他的之类,他里面维护了一个 private final ClassLoader parent; 字段和loadClass方法

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {//1.首先,检查类是否已加载// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {//如果父类加载器不为空,则优先委派父类进行加载if (parent != null) {c = parent.loadClass(name, false);} else {//如果父类加载器为空,则查找 Bootstrap 类加载器,如果找不到则返回nullc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();//如果c == null 说明父类加载失败,则自己加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

这里需要注意的一点是:这些类加载器是没有继承关系的,而是通过维护一个parent成员变量来体现父子关系(组合模式)。上面代码的大意是

  • 首先ClassLoader会检查某个class是否已经被加载,已经加载的类会存储到JVM中,则无需加载直接返回Class,这里是使用c++去实现的
  • 如果父类加载器加载结果为null,则会调用自己的类加载器方法findClass去加载

这里是典型的双亲委派设计模式,这样设计有什么目的呢?一个是为了防止类重复加载,二个是安全性问题

  • 防止类重复加载:父类加载器如果已经加载了某个class,那么子类加载器将不再会加载
  • 安全性问题 : 试想如果我们自己写了一个类java.lang.String 那么jvm会不会采用我们的String而不采用JDK自己带的String呢,答案是不会的。因为BootStrapClassLoader 优先把String加载进JVM中,我们自己的String根本就不会生效。

Tomcat Class Loader 打破双亲委派

对于Tomcat而言它是打破了JVM的双亲委派的。他自定义了自己的类加载器如下:
在这里插入图片描述
Tomcat定义了自己的类加载器去打破双亲委派,它主要解决3个问题

  • 一个Tomcat需要加载不同的项目代码,那么不同的项目中肯定有相同名字的类,但是功能又不同,这些类如何做代码隔离
  • 一个Tomcat需要加载不同的项目代码,对于一些公共的类,在不同的项目中否需要重复加载?答案是否定的,否则JVM会日益膨胀,那么如何做到公共的class只加载一份呢,并且不同的项目需要共享这些公共的class.
  • Tomcat本省的代码也是需要类加载器去加载

要解决这些问题就需要说道Tomcat自定义的ClassLoader了他们的职责如下

  • commonLoader : 加载基础的类,这些类是tomcat和app项目共用的,在catalina.properties中定义了common.loader属性该属性指定一些lib路径,CommonLoader会从这些目录中加载一些基础的class。
  • catalinaLoader :加载Tomcat私有的类,app项目不可见,在catalina.properties中定义了server.loader属性该属性指定一些lib路径,catalinaLoader会从这些目录中加载class。
  • sharedLoader : 加载共享的类,多个app项目都可见,在catalina.properties中定义了shared.loader属性该属性指定一些lib路径,catalinaLoader会从这些目录中加载class。
  • WebappClassLoader ::每个 Web 应用程序都有一个与之关联的 Web 应用程序类加载器。它负责加载 Web 应用程序自身的类库,专门负责加载servelt应用,每个应用都有自己的WebappClassLoader,相互隔离,但它并不遵循双亲委派模型

WebappClassLoader : 实现项目隔离

WebappClassLoader是针对每个Servlet项目都有一个,这样可以实现项目之间的相互隔离,比如不同的项目中都用到Spring,但是他们使用的Spring版本不一杨,有了WebappClassLoader之后也能相安无事,因为class是相互隔离的。所以:不同的加载器加载的类是认为不同的,那怕类名是相同的。而如果同一个ClassLoader中出现了2个相同的类,ClassLoader也只会加载一次

SharedClassLoader : 实现class共享
多个项目之间势必有一些共享的类,Tomcat是如何实现不同app之间类的共享的类,SharedClassLoader 作为 WebappClassLoader的父类加载器,如果WebappClassLoader没有加载到某个类(这个类可能是共享的)就会委托父类加载器 SharedClassLoader去加载,SharedClassLoader会在指定目录下加载一些共享的类返回给WebappClassLoader,这样就实现了不同的项目之间共享类。

CatalinaClassloader :实现Tomcat私有加载

Tomcat自身的类并没有使用WebappClassLoader来加载,而是专门设计了一个CatalinaClassloader来加载,这样就可以实现Tomcat本身的类和APP的类进行隔离,那么如果Tomcat和APP之间需要共享一些类怎么办呢?Tomcat设计了commonLoader类加载器来实现 Tomcat和各个APP之间的类共享。commonLoader作为CatalinaClassloader 和 SharedClassLoader的父加载器,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离。

在这里插入图片描述
下面我们来看一下 WebAppClassLoader 是如何加载Class的,核心代码在其父类:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean),源码如下

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {Class<?> clazz = null;...// (0) Check our previously loaded local class cache//(0) 检查我们之前加载的本地类缓存clazz = findLoadedClass0(name);//拿到JAVA的类加载器 ExtClassLoaderClassLoader javaseLoader = getJavaseClassLoader();...//委派JavaSe的ExtClassLoader去尝试加载clazz = javaseLoader.loadClass(name);...//调用自己的findClass来加载clazz = findClass(name);}

上面代码我精简了一下,大概流程是

  1. 先从缓存中去检查该类是否已经被加载,如果已经加载了就会直接返回不会再加载
  2. 会找到Java的ExtClassLoader去加载,为什么呢?因为所有类都需要一个Object.class才可以使用,所以必须先加载JDK一些基础的东西。但是这里没有使用Java的AppClassLoader去加载,如果使用AppClassLoader去加载那就没有打破双亲委派,很显然这里打破了。
  3. 如果ExtClassLoader加载不到那么这个类可能是我们自己的类的,就会调用findClass方法去加载

下面是org.apache.catalina.loader.WebappClassLoaderBase#findClass 源码

 public Class<?> findClass(String name) throws ClassNotFoundException {//在内部找classclazz = findClassInternal(name);...if (clazz == null && hasExternalRepositories) {try {//委托父类加载clazz = super.findClass(name);...}//父类也没找到就抛出异常if (clazz == null) {if (log.isTraceEnabled()) {log.trace("    --> Returning ClassNotFoundException");}throw new ClassNotFoundException(name);}
}

这里的大概含义就是:现在项目内部加载class,如果自己没加载到再委托父加载器去加载。稍微归纳一下加载流程如下

  1. 先检查缓存,确定该类是否已经被加载
  2. 委托ExtClassLoader去加载(需要JDK环境)
  3. 调用findClass 自己去加载
  4. 找不到再委托super父类加载器去加载
    在这里插入图片描述

总结:为什么Tomcat需要打破双亲委派

Tomcat 并没有完全打破 Java 的双亲委派模型,而是对其进行了扩展和补充,以适应 Web 应用程序的特殊需求。Tomcat 打破双亲委派模型的主要原因有以下几点:

  • 隔离性:
    Web 应用程序通常希望自己的类库(位于 WEB-INF/lib 和 WEB-INF/classes 目录下)与容器提供的类库和其他应用程序的类库完全隔离。如果完全遵循双亲委派模型,那么应用程序可能会意外地加载到容器或其他应用程序的类,导致版本冲突或不可预期的行为。
  • 热替换和重新加载:
    Tomcat 支持在不重启整个容器的情况下重新加载或替换 Web 应用程序。为了实现这一功能,Tomcat 需要为每个 Web 应用程序提供一个独立的类加载器,以便能够单独卸载和重新加载应用程序的类。
  • 自定义类加载:
    Tomcat 允许管理员通过配置来指定额外的共享库(位于 CATALINA_HOME/lib 目录下),这些库可以被所有的 Web 应用程序共享。为了实现这一功能,Tomcat 需要一个额外的类加载器(如 Catalina 类加载器)来加载这些共享库,并在需要时将它们提供给 Web 应用程序类加载器。
  • 处理复杂的类库依赖:
    在某些情况下,Web 应用程序可能依赖于特定版本的类库,而这些版本可能与 Tomcat 容器或其他应用程序的类库版本不同。为了处理这种复杂的类库依赖关系,Tomcat 需要提供一种机制来确保每个应用程序加载到正确的类库版本。
    Tomcat 并没有完全打破双亲委派模型,而是在其基础上增加了额外的类加载器层次结构,并通过特定的加载策略来实现上述功能。这种设计使得 Tomcat 能够在保持类加载灵活性和隔离性的同时,也支持了 Web 应用程序的复杂性和动态性。

需要注意的是,虽然 Tomcat 的类加载器设计在一定程度上打破了双亲委派模型,但它仍然遵循了 Java 的类加载机制的基本原则,包括安全性、可靠性和可维护性等。因此,在使用 Tomcat 时,开发人员仍然需要注意类加载相关的最佳实践和潜在问题。

有点懒,不想写太长了,就写到这里把,觉得可以给个好评

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

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

相关文章

华为eNSP小型园区网络配置(下)

→跟着大佬学习的b站直通车&#xff0c;感谢大佬← →华为eNSP小型园区网络配置&#xff08;上&#xff09;← 目标1&#xff1a;telnet配置 R1 # interface GigabitEthernet0/0/2ip address 100.1.1.2 255.255.255.0 # user-interface vty 0 4authentication-mode aaa # aaa…

英语新概念2-回译法-lesson12

第一次翻译 &#xff08;稀巴烂&#xff09; Our neiborhood,Capitain Charles Alison,will sail from P. We will ______ in the _. He will sit in his small boat, Topsail,Topsail is a famous boat. It has been across the A many times. Alison will sail at 8 o’cloc…

05-06 周一 Shell工程目录划分和开发最佳实践

05-06 周一 Shell工程目录划分和开发最佳实践 时间版本修改人描述2024年5月6日10:34:13V0.1宋全恒新建文档2024年5月6日11:07:12V1.0宋全恒完成 简介 之前楼主曾经完成过一个shell工程的开发&#xff0c;记得当时项目名称叫做campus-shell&#xff0c;主要是用来一键完成多个模…

【信息安全管理与评估】某年“信息安全管理与评估”第二阶段:Windows应急响应例题

文章目录 1、提交攻击者的IP地址&#xff1b;2、识别攻击者使用的操作系统&#xff1b;3、找出攻击者资产收集所使用的平台&#xff1b;4、提交攻击者目录扫描所使用的工具名称&#xff1b;5、提交攻击者首次攻击成功的时间&#xff0c;格式&#xff1a;DD /MM/YY:HH:MM:SS&…

SpringBoot中HandlerInterceptor拦截器的构建详细教程

作用范围&#xff1a;拦截器主要作用于Spring MVC的DispatcherServlet处理流程中&#xff0c;针对进入Controller层的请求进行拦截处理。它基于Java的反射机制&#xff0c;通过AOP&#xff08;面向切面编程&#xff09;的思想实现&#xff0c;因此它能够访问Spring容器中的Bean…

【Fastadmin】后台角色组权限问题(multi,开关switch,控制器新增方法)

1.列表开关类型的权限 如图&#xff1a; 此类开关请求的方法为multi 开关在点击的时候默认是只允许修改数据库的status字段的&#xff0c;如果我们开关不是status字段&#xff0c;我们需要在服务端对应的控制器中定义protected $multiFields"id,name,swith";&#x…

️测试问我:为啥阅读量计数这么简单的功能你都能写出bug?

前言 可乐他们团队最近在做一个文章社区平台,由于人手不够,后端部分也是由前端同学来实现,使用的是 nest 。 今天他接到了一个需求,就是在用户点开文章详情的时候,把阅读量 +1 ,这里不需要判断用户是否阅读过,无脑 +1 就行。 它心想:这么简单,这不是跟 1+1 一样么。…

1-2 ARM单片机GPIO

def&#xff1a;通用输入输出口 GPIO输出模式原理讲解 1&#xff1a;推挽输出 2&#xff1a;复用推挽输出 电流最大是20mA&#xff0c;对于单片机来说总体的输出是由范围的 开漏/复用开漏输出 外部接上拉电阻的开漏输出 线与的概念 注&#xff1a; 与的概念&#xff1a;全1为1&…

3d模型实体显示有隐藏黑线?---模大狮模型网

在3D建模和设计领域&#xff0c;细节决定成败。然而&#xff0c;在处理3D模型时&#xff0c;可能会遇到模型实体上出现隐藏黑线的问题。这些黑线可能影响模型的视觉质量和呈现效果。因此&#xff0c;了解并解决这些隐藏黑线的问题至关重要。本文将探讨隐藏黑线出现的原因&#…

Java基础教程 - 4 流程控制

更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 更好的阅读体验&#xff1a;点这里 &#xff08; www.doubibiji.com &#xff09; 4 流程控制 4.1 分支结构…

16、ESP32 Web

Web 服务器具有移动响应能力&#xff0c;可以使用本地网络上的任何设备作为浏览器进行访问。 示例功能&#xff1a; 构建 Web 服务器控制连接到 ESP32 的 LED在本地网络的浏览器上输入 ESP32 IP 地址访问 Web 服务器通过单击 Web 服务器上的按钮&#xff0c;更改 LED 状态 //…

VS Code中PlatformIO IDE的安装并开发Arduino

VS Code中PlatformIO IDE的安装并开发Arduino VS Code的安装 略 PlatformIO IDE的安装 PlatformIO IDE是是什么 PlatformIO IDE 是一个基于开源的跨平台集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门用于嵌入式系统和物联网&#xff08;IoT&#xff09;开发。…

CANdela/Diva系列1--CANdela Studio的基本介绍

大家好&#xff0c;这个系列主要给大家介绍跟诊断相关的Vector 工具CANdela和Diva&#xff0c;首先介绍CANdela。 目录 1.CANdela的简介&#xff1a; 2.如何打开CANdela 工程&#xff1a; 3.CANdela工程的详细介绍&#xff1a; 3.1 工具栏的介绍&#xff1a; 3.2 工作树的…

全新拼团模式 你一定没见过 白拿产品还有收益!

在七星拼团模式中&#xff0c;奖励制度是其核心吸引力之一。今天&#xff0c;我们将深入探讨这一模式的三种奖励&#xff1a;直推奖、滑落奖和出局奖&#xff0c;以及它们背后的互助机制。 奖励规则概述 首先&#xff0c;让我们了解一下奖励的具体规则。假设商品售价为499元&a…

自动化运维管理工具 Ansible-----【inventory 主机清单和playbook剧本】

目录 一、inventory 主机清单 1.1inventory 中的变量 1.1.1主机变量 1.1.2组变量 1.1.3组嵌套 二、Ansible 的脚本 ------ playbook&#xff08;剧本&#xff09; 2.1 playbook介绍 2.2playbook格式 2.3playbooks 的组成 2.4playbook编写 2.5运行playbook 2.5.1ans…

JavaScript中的扩展操作符作用是什么,有什么含义?

在 JavaScript 中&#xff0c;扩展操作符允许一个表达式在某些地方展开成多个元素。这个特性在 ES2015 (也叫做 ES6) 中被引入到 JavaScript 语言中&#xff0c;并广泛用于数组和对象。在您的代码示例中&#xff0c;它被用于对象。 对象中的扩展操作符 在对象字面量中使用扩展…

SolidWorks进行热力学有限元分析二、模型装配

1.先打开软件&#xff0c;新建装配体 2.选中你要装配的零件&#xff0c;直接导入就行 3.鼠标点击左键直接先放进去 4.开始装配&#xff0c;点配合 5.选择你要接触的两个面&#xff0c;鼠标右键确定&#xff0c;然后把剩下的面对齐一下就行了 6.搞定

Day1| Java基础 | 1 面向对象特性

Day1 | Java基础 | 1 面向对象特性 基础补充版Java中的开闭原则面向对象继承实现继承this和super关键字修饰符Object类和转型子父类初始化顺序 多态一个简单应用在构造方法中调用多态方法多态与向下转型 问题回答版面向对象面向对象的三大特性是什么&#xff1f;多态特性你是怎…

嵌入式学习

笔记 作业 将一张bmp图片修改成德国国旗。 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #in…

基于vue.js+thymeleaf模板引擎+ajax的注册登陆简洁模板(含从零到一详细介绍)

文章目录 前言1、数据库准备2、工具类与相关基类使用2.1、工具类2.2、相关基类 3、web包目录说明4、注册功能设计&#xff08;本文核心部分&#xff09;4.1、注册页面设计4.2、注册逻辑设计 5、登陆功能设计5.1、登陆页面设计5.2、登陆逻辑设计 6、运行效果图 前言 大多数的网…