Tomcat类加载机制详解

news2024/9/20 19:42:00

1.Tomcat类加载机制详解

1.1 JVM类加载器

Java中有 3 个类加载器,另外你也可以自定义类加载器

  • 引导(启动)类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序(系统)类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包
public class ClassLoaderDemo {

    public static void main(String[] args) {
        // BootstrapClassLoader
        System.out.println(ReentrantLock.class.getClassLoader());
        // ExtClassLoader
        System.out.println(ZipInfo.class.getClassLoader());
        // AppClassLoader
        System.out.println(ClassLoaderDemo.class.getClassLoader());

        // AppClassLoader
        System.out.println(ClassLoader.getSystemClassLoader());
        // ExtClassLoader
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        // BootstrapClassLoader
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

    }
}

1.2 双亲委派机制

JVM类加载器是有亲子层级结构的,如下图

        这种类加载机制其实就是双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

ClassLoader#loadClass源码分        

我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。

3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载

public abstract class ClassLoader {
                                                                                                                                                                                               
    // 每个类加载器都有个父加载器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) {
        // 查找一下这个类是不是已经加载过了
        Class<?> c = findLoadedClass(name);
        // 如果没有加载过
        if( c == null ){
          // 先委托给父加载器去加载,注意这是个递归调用
          if (parent != null) {
              c = parent.loadClass(name);
          }else {
              // 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          }
        }

        // 如果父加载器没加载成功,调用自己的 findClass 去加载
        if (c == null) {
            c = findClass(name);
        }
        return c;
    }

    protected Class<?> findClass(String name){
       //1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存
          ...
       //2. 调用 defineClass 将字节数组转成 Class 对象
       return defineClass(buf, off, len);
    }

    // 将字节码数组解析成一个 Class 对象,用 native 方法实现
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }

}

思考:为什么要设计双亲委派机制

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

1.3 Tomcat 如何打破双亲委派机制

Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委派机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。

findClass方法

我们先来看看 findClass 方法的实现

public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    Class<?> clazz = null;
    try {
            //1. 先在 Web 应用目录下查找类 
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
    }
    
    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
    }
    
    //3. 如果父类也没找到,抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }
    return clazz;
}

在 findClass 方法里,主要有三个步骤:

1)先在 Web 应用本地目录下查找要加载的类。

2)如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。

3)如何父加载器也没找到这个类,抛出 ClassNotFound 异常。

loadClass 方法

接着我们再来看 Tomcat 类加载器的 loadClass 方法的实现

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        //1. 先在本地 cache 查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 从系统类加载器的 cache 中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
 
        // 3. 尝试用 ExtClassLoader 类加载器类加载,为什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 尝试在本地目录搜索 class 并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }
    
    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
}

loadClass 方法稍微复杂一点,主要有六个步骤:

1)先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。

2)如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。

3)如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委派机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。

4)如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。

5)如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。

6)如果上述加载过程全部失败,抛出 ClassNotFound 异常。

从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委派机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委派机制了,这就是 Tomcat 类加载器的巧妙之处。

1.4 Tomcat如何隔离Web应用

Tomcat 作为 Servlet 容器,它负责加载我们的 Servlet 类,此外它还负责加载 Servlet 所依赖的 JAR 包。并且 Tomcat 本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。首先让我们思考这一下这几个问题:

1)假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。

2)假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。

3)跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。

思考:Tomcat 是如何解决这些问题的?

Tomcat类加载器的层次结构

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;

WebAppClassLoader

我们先来看第 1 个问题,假如我们使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。

因此 Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。

SharedClassLoader

我们再来看第 2 个问题,本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

CatalinaClassloader

我们来看第 3 个问题,如何隔离 Tomcat 本身的类和 Web 应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?

CommonClassLoader

老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

Spring 的加载问题

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring 是通过调用Class.forName来加载业务类的,我们来看一下 forName 的源码:

public static Class<?> forName(String className) {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以看到在 forName 的函数里,会用调用者也就是 Spring 的加载器去加载业务类。

我们在前面提到,Web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免重复加载。Spring 作为共享的第三方 JAR 包,它本身是由 SharedClassLoader 来加载的,Spring 又要去加载业务类,按照前面那条规则,加载 Spring 的类加载器也会用来加载业务类,但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,这该怎么办呢?

线程上下文加载器

于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此 Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。Spring 取线程上下文加载的代码如下:

cl = Thread.currentThread().getContextClassLoader();

        线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的.

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

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

相关文章

流量掘金付费进群源码,最新无人直播变现玩法

外面1800流量掘金付费进群搭建 最新无人直播变现玩法 流量掘金付费进群网站源码 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89662670 更多资源下载&#xff1a;关注我。

消化学科的领军人物陈烨教授在会议上作了《幽门螺杆菌的规范检测与质控》的专题报告

由广东省药学会主办的“第十九届消化疾病诊疗会暨胃肠疾病药物临床研究交流会”于2024年8月8日-9日在广东省深圳市召开。陈烨教授&#xff0c;作为消化学科的领军人物、中华医学会消化病学分会的常务委员&#xff0c;以及全国幽门螺杆菌学组的组长&#xff0c;在会议上作了《幽…

用爬虫玩转石墨文档细解

​ ​ 您好&#xff0c;我是程序员小羊&#xff01; 前言 石墨文档是一款受欢迎的在线协作工具&#xff0c;它允许多人实时编辑和共享文档。通过爬虫技术&#xff0c;我们可以自动化地获取石墨文档中的内容&#xff0c;进行数据分析或备份。不过&#xff0c;在使用爬虫技术时&a…

Nmap扫描六种端口状态介绍

传统意义上的端口状态只有 open 或 close 两种。网络发现扫描最常用的工具是Nmap&#xff0c;可以提供更细分的端口状态&#xff0c;共计六种&#xff0c;分别为open, closed, filtered, unfiltered, open|filtered, or closed|filtered。 1. 端口状态介绍 开放的&#xff08;o…

艾多美携手三星SDS,共筑物流优化与数字化转型新篇章!

8月20日下午&#xff0c;艾多美公司董事长朴炳宽受邀出席了由山东省人民政府、韩国产业通商资源部联合主办的“山东省—韩国经贸合作交流会”。在此次盛会上&#xff0c;艾多美中国与全球领先的IT解决方案提供商三星SDS达成了具有重要里程碑意义的战略合作&#xff0c;双方将共…

安防监控EasyCVR视频监控汇聚管理平台登录1分钟之后自动退出是什么原因?

EasyCVR视频监控汇聚管理平台是一款针对大中型项目设计的跨区域网络化视频监控集中管理平台。该平台不仅具备视频资源管理、设备管理、用户管理、网络管理和安全管理等功能&#xff0c;还支持多种主流标准协议&#xff0c;如GB28181、RTSP/Onvif、RTMP、部标JT808、GA/T 1400协…

docker基本环境搭建

前面在虚拟机centos中搭建的fastdfs和minio分布式文件存储服务都是手动编译安装的&#xff0c;为了方便后续学习&#xff0c;本地开发环境的中间件服务部署&#xff0c;我们将交给docker来部署。下面先进行docker环境搭建。 后续相关教程&#xff08;待更新&#xff09;&#…

QT中通过TCP协议多线程的文件传输(客户端)

首先&#xff0c;新建一个项目&#xff0c;我命名为了SendFileClient 首先我们要在pro文件中 代码第一行加入network的后缀 一、窗口搭建 如图所示&#xff0c;在第一个QWidget中让客户端输入IP&#xff0c;端口号 连接服务器 第二个Qwidget 设置一个LineEdit,供客户端选择要…

二维中,若直线上两点q1和q2,输入一个点P1,求P1在直线上的垂点

一、计算过程 在二维空间中&#xff0c;若给定直线上两点Q1和Q2以及一个点P1&#xff0c;要求出点P1在直线上的垂点&#xff0c;可以通过以下步骤进行&#xff1a; ‌1、判断点P1是否在直线q1-q2上‌&#xff1a; 首先&#xff0c;需要判断点P1是否位于直线Q1-Q2上。这可以通过…

【Linux】实现三个迷你小程序(倒计时,旋转指针,进度条)

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:Linux ⚙️操作环境:Xshell (操作系统:CentOS 7.9 64位) 目录 &#x1f4cc;倒计时小程序 &#x1f38f;项目效果展示 &#x1f38f;项目实现思路 &#x1f38f;项目完整代码 &#x1f4cc;旋转指针小程序 &#x…

视频监控接入汇聚平台如何根据客户要求定制资源树结构和资源的任意排序

目录 一、需求描述 1.1视频监控资源树 1.2客户要求 二、市场上产品常用处理方法 2.1 常用处理方法 &#xff08;1&#xff09;按笔画数排序 &#xff08;2&#xff09;按拼音排序 &#xff08;3&#xff09;按字典序排序 &#xff08;4&#xff09;按首字母排序 2.2 …

使用预训练的 ONNX 格式的目标检测模型(基于 YOLOv8n-pose)姿态监测

具体步骤如下&#xff1a; 加载图像&#xff1a; 从指定路径读取一张图像&#xff08;这里假设图像名为bus.jpg&#xff09;。将图像从 BGR 颜色空间转换为 RGB 颜色空间。 图像预处理&#xff1a; 计算图像的高度、宽度&#xff0c;并确定其中的最大值作为新图像的边长。创建一…

C语言新手小白详细教程:冒泡排序

&#x1f30f;个人博客主页&#xff1a;意疏-CSDN博客 希望文章能够给到初学的你一些启发&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏支持一下笔者吧&#xff5e; 阅读指南&#xff1a; 开篇说明冒泡排序简介冒泡排序代码 开篇说明 本文我们来介绍冒…

编码器精度

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 文章目录 前言一、影响因素二、编码器精度三、位置因素四、环境因素五、磁编码器 前言 送给大学毕业后找不到奋斗方向的你&…

《九界ol游戏源码》(游戏源码+客户端+服务端+工具+视频教程)百度云盘下载

游戏制作精美&#xff0c;和天龙八部用一样的游戏引擎&#xff0c;就是ogre。有兴趣的朋友&#xff0c;可以下载来收藏研究。 《九界ol游戏源码》&#xff08;游戏源码客户端服务端工具视频教程&#xff09; 下载地址&#xff1a; 链接: https://pan.baidu.com/s/1KIWSVYyAcSpY…

案例分享—医疗行业国外优秀界面设计案例

医疗行业UI网页设计需注重用户体验与可访问性&#xff0c;确保页面布局清晰、导航简洁明了&#xff0c;以便用户快速找到所需信息。同时&#xff0c;设计应符合医疗行业规范&#xff0c;传递出专业、可信的形象。 在医疗行业UI网页设计中&#xff0c;色彩搭配与视觉元素的选择至…

【大模型从入门到精通35】开源库框架LangChain 利用LangChain构建聊天机器人2

这里写目录标题 利用LangChain构建具备记忆功能的对话检索链设置对话历史记忆构建对话检索链处理问题并生成答案 创建基于文档的问答聊天机器人初始设置和导入文档加载和处理 利用LangChain构建具备记忆功能的对话检索链 设置对话历史记忆 为了使问答系统能够记住对话的上下文…

计算机网络-2-tcpip协议

1.说说 TCP/IP 四层模型&#xff1f; TCP/IP&#xff08;Transmission Control Protocol/Internet Protocol&#xff09;模型是一种用于描述互联网通信的协议层次结构。它分为四个主要层次&#xff0c;每个层次都定义了不同的协议来实现特定的功能。下面是TCP/IP模型各层的常用…

PCIE-flit mode retry

下一个即将发送的seq num: 下一个即将发送的ack或者nak的seq num: Tx发送exp seq num的个数: Tx发送nak的个数 下一个期望收到的flit的seq num&#xff0c;注意是指下个期望收到的有效的、non-idle、non_duplictae的flit: 收到的flit的真实的seq num&#xff08;implicit…

控制台小游戏—扫雷

引言&#xff1a; 在前面一篇中&#xff0c;小编介绍了如何将代码分装在多个文件中的操作。在本篇文章中&#xff0c;小编将介绍一个广受欢迎的小游戏&#xff0c;以帮助大家熟悉这个操作。这个小游戏不仅可以让我们熟悉将代码放在多个文件中的操作&#xff0c;还可以加深我们…