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

news2025/1/11 7:40:36

前言

上篇文章《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 completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                	//1.初始化Tomcat
                    bootstrap.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 reporting
            if (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() method
        if (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 loader
        if (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 loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果父类加载器不为空,则优先委派父类进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果父类加载器为空,则查找 Bootstrap 类加载器,如果找不到则返回null
                        c = 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 stats
                    sun.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的类加载器 ExtClassLoader
    ClassLoader 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 {
	//在内部找class
	clazz = 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.coloradmin.cn/o/1649202.html

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

相关文章

5月7号(信息差)

&#x1f30d;首次&#xff0c;西湖大学用蛋白质语言模型定向改造碱基编辑器&#xff0c;登Cell子刊 https://www.jiqizhixin.com/articles/2024-05-07-10 &#x1f384; 哈马斯宣布同意停火提议 https://finance.eastmoney.com/a/202405073067687785.html ✨ 中国将对…

【陀螺仪JY61P维特智能】通过单片机修改波特率和角度参考的方法

根据官方文档&#xff1a; 修改波特率 1.解锁:FF AA 69 88 B5 1.1延时200ms 2.修改波特率:FF AA 04 06 00 2.1切换已修改的波特率然后重新发送解锁和保存指令 2.2解锁:FF AA 69 88 B5 2.3延时200ms 4.保存: FF AA 00 00 00 XY轴角度参考 角度参考是以传感器当前的实际位置&…

IDEA远程连接docker服务,windows版docker desktop

1.windows上安装docker desktop docker desktop下载地址&#xff1a;Docker Desktop: The #1 Containerization Tool for Developers | Docker 有的windows系统不支持安装docker desktop 安装完之后我们可以直接打开&#xff0c;可以选择不登录使用 我们用IDEA连接到docker …

python菜鸟级安装教程 -下篇(安装编辑器)

来来~接着上篇的来~ 安装好python.exe之后&#xff0c;我们可以根据cmd命令窗口&#xff0c;码代码。 这算最简单入门了~ 如果我们在安装个编辑器。是什么效果&#xff0c;一起体验一下吧 第一步&#xff0c;下载编辑器&#xff0c;选择官网&#xff0c;下载免费版本入门足…

语音识别--kNN语音指令识别

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

【LAMMPS学习】八、基础知识(5.8)LAMMPS 中热化 Drude 振荡器教程

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

TriCore TC162 Archievture Volume 笔记

说明 本文是 英飞凌 架构文档 TriCore TC162P core archiecture Volume 1 of 2 (infineon.com) 的文笔&#xff0c;稍作整理方便查阅&#xff0c;错误之处&#xff0c;还请指正&#xff0c;谢谢 :) 1. Architecture 2. General Purpose & System Register 名词列表&#…

gin-vue-blog 前后端分离项目(已经部署)

gin-vue-blog 前台&#xff1a; 后台&#xff1a; 1.数据库设计&#xff1a;https://blog.csdn.net/m0_73337964/article/details/138137629?spm1001.2014.3001.5501 2.RESTFUL API路由实现&#xff1a;https://blog.csdn.net/m0_73337964/article/details/138321631?spm1…

基于Spring Boot的家具网站设计与实现

基于Spring Boot的家具网站设计与实现 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 系统前台主界面图&#xff0c;用户可进入家具网站可查看…

9.4.k8s的控制器资源(job控制器,cronjob控制器)

目录 一、job控制器 二、cronjob控制器 一、job控制器 job控制器就是一次性任务的pod控制器&#xff0c;pod完成作业后不会重启&#xff0c;其重启策略是&#xff1a;Never&#xff1b; 简单案例 启动一个pod&#xff0c;执行完成一个事件&#xff0c;然后pod关闭&#xff1b;…

抖音小店个人店和个体店有什么不同?区别问题,新手必须了解!

哈喽~我是电商月月 新手开抖音小店入驻时会发现&#xff0c;选择入驻形式时有三个选择&#xff0c;个人店&#xff0c;个体店和企业店 其中&#xff0c;个人店和个体店只差了一个字&#xff0c;但个人店不需要营业执照&#xff0c;是不是入驻时选择个人店会更好一点呢&#x…

PXE高效批量装机

一、PXE的概述 PXE是由Inter 公司开发的网络引导技术&#xff0c;工作在Client / Server 模式。允许客户机通过网络从远程服务器下载引导镜像&#xff0c;并加载安装文件或者整个操作系统。 1.1PXE优点 规模化&#xff1a;同时装配多台服务器 自动化&#xff1a;安装系统&am…

使用DependencyCheck工具检测JAR依赖包的安全漏洞

引言 Dependency-Check 是一个开源工具,用于检测软件项目中使用的第三方库和组件是否存在已知的安全漏洞。它可以帮助开发团队及时发现和解决项目中的潜在安全风险,从而提高软件的安全性。 该工具通过分析项目的依赖关系,识别其中使用的第三方库和组件,并与已知的漏洞数据…

【牛客】Tokitsukaze and Average of Substring

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 前缀和。 开一个int类型的前缀和数组pre[30][N]&#xff08;pre[i][j]表示某字符转成的数字 i 在一段区间的前缀个数。因为字母表有‘a’~z…

PE文件(四)FileBuffer-ImageBuffer

文件执行的总过程 当文件从硬盘中读入虚拟内存&#xff08;FileBuffer&#xff09;中时&#xff0c;文件数据会被原封不动的复制一份到虚拟内存中&#xff0c;然后进行拉伸对齐。此时虚拟内存中文件数据叫做文件印象或者内存印象&#xff0c;即ImageBuffer。此时ImageBuffer中…

KDTree空间搜索算法学习

目录 KDTree&#xff08;K-Dimensional Tree&#xff09;原理步骤空间索引建立例子[^1] 相关包案例[^2]数据KDTree 识别轨道衔接出行轨道衔接单车骑行范围分析结果保存 KDTree&#xff08;K-Dimensional Tree&#xff09;原理 将需要匹配的 K 维空间点建立 K 维树空间索引&…

FSC森林认证是什么?

FSC森林认证&#xff0c;又称木材认证&#xff0c;是一种运用市场机制来促进森林可持续经营&#xff0c;实现生态、社会和经济目标的工具。FSC森林认证包括森林经营认证&#xff08;Forest Management, FM&#xff09;和产销监管链认证&#xff08;Chain of Custody, COC&#…

Compose 状态管理

文章目录 Compose 状态管理概述使用MutableStaterememberStatelessComposable & StatefulComposable状态提升rememberSaveable支持parceable不支持parceable 使用ViewModelViewModelProvider.Factory 使用Flow Compose 状态管理 概述 当应用程序的状态发生变化时&#xf…

2024.5.7

槽函数声明 private slots:void on_ed_textChanged();void on_pushButton_clicked(); }; 槽函数定义 void Widget::on_ed_textChanged()//文本框 {if(ui->ed1->text().length()>5&&ui->ed2->text().length()>5){ui->pushButton->setStyleSh…

纯血鸿蒙APP实战开发——手写绘制及保存图片

介绍 本示例使用drawing库的Pen和Path结合NodeContainer组件实现手写绘制功能。手写板上完成绘制后&#xff0c;通过调用image库的packToFile和packing接口将手写板的绘制内容保存为图片&#xff0c;并将图片文件保存在应用沙箱路径中。 效果图预览 使用说明 在虚线区域手写…