Tomcat类加载器揭秘:“重塑”双亲委派模型

简介: 【10月更文挑战第5天】本文深入探讨了Tomcat类加载器的工作机制及其对经典双亲委派模型的重塑。传统上,Java类加载器遵循双亲委派模型以确保类的安全性与唯一性,但在应用服务器场景下,这种模式需调整以应对复杂需求。Apache Tomcat作为流行Java Web应用服务器,通过自定义类加载器打破了常规。文章首先回顾了类加载器基础知识,接着详细分析了Tomcat如何利用自定义WebAppClassLoader解决多Web应用间的类隔离问题,并介绍了其实现细节。通过调整类加载顺序,Tomcat既保留了核心类库的安全性,又实现了灵活的类加载策略。最后,总结了Tomcat类加载器的关键流程及优势。

Tomcat类加载器揭秘:“重塑”双亲委派模型

在Java世界中,类加载器作为程序运行时动态加载类的基石,遵循着经典的双亲委派模型原则,这一设计确保了类的唯一性和安全性

然而,在某些特殊应用场景下,如应用服务器领域,传统的双亲委派模型需要被巧妙地“重塑”以满足更复杂的需求

Apache Tomcat,作为最流行的Java Web应用服务器之一,正是这样一个打破常规、挑战传统的典范

本文,我们将踏上一段深度探索之旅,揭秘Tomcat如何以及为何要打破Java的双亲委派模型

双亲委派模型

先来复习下类加载器相关知识(也可以查看类加载器文章):

JVM运行时遇到类需要检测类是否加载,如果未加载则将类信息加载到运行时的方法区并生成Class对象

在这个过程中,JVM通过类加载器进行类加载

类加载器分为引导(Bootstrap)、扩展(Ext)、应用(App)类加载器(ClassLoader)

引导类加载器由C++实现,用于加载核心类库

扩展类加载器用于加载扩展库,应用类加载器则常用于加载我们自定义的类

扩展、应用类加载器由Java代码实现,组合为父子关系(不是继承)

默认情况下类加载会使用双亲委派模型:进行类加载时将类交给父类尝试加载,如果父类不加载再由自己加载,当自己也无法加载时抛出ClassNotFoundException异常

双亲委派模型下类加载的顺序为:引导 Boot -> 扩展 Ext -> 应用 App

ClassLoader.loadClass

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
   
    //加锁保证类加载
    synchronized (getClassLoadingLock(name)) {
   
        //检查类是否加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
   
            //类未加载则开始进行类加载
            long t0 = System.nanoTime();
            try {
   
                //父类加载器不为空,交给父类加载
                if (parent != null) {
   
                    c = parent.loadClass(name, false);
                } else {
   
                    //父类加载器为空,说明当前加载器为最顶级的引导类加载器,调用本地方法进行加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
   
            }

            //父类没有加载,由自己加载
            if (c == null) {
   
                long t1 = System.nanoTime();

                //进行类加载
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
   
            resolveClass(c);
        }
        return c;
    }
}

如果我们编写一个全限定类名相同的核心类库时,比如java.lang.Object,并调用其中的main方法时,程序会报错

错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

这是因为双亲委派模型会防止java.lang.Object这种核心类库被篡改,它们由父类加载器进行加载,因此加载时找不到我们编写的main方法

Tomcat类加载器

既然双亲委派模型能够防止核心类库被篡改,那么Tomcat为啥还要打破双亲委派模型呢?

在Tomcat中(Servlet规范),允许多Web应用(多context容器)

如果多Web应用下依赖的类名相同但这两个类不是同一个类(功能不同),该怎么办?又或者说依赖的三方类,类名相同但版本不同该怎么办?

而有些类又需要Web应用(context容器)共享该怎么办?

通过类加载器可以解决隔离的问题,不同类加载器加载的类,即使全限定类名相同那它们也不是同一个类

因此在JVM中,判断类是否相同,必须全限定类名相同且类加载器相同

为了解决这些问题,Tomcat需要使用自定义类加载器对类进行隔离

前文21张图解析Tomcat运行原理与架构全貌介绍过Context容器中有一个Loader组件,它就是Tomcat在Context容器中的类加载器

Tomcat使用WebAppClassLoader对应每个Context容器下的Loader,来进行容器间类的隔离

而如果容器间需要共享相同的类,再增加个共享的类加载器SharedClassLoader作为WebAppClassLoader的父类

还要其他类似隔离的类加载器就不再说了(一层不够就再加一层)

源码解析

在Tomcat启动容器时,会启动后台定时检查的任务

ContainerBase.threadStart

protected void threadStart() {
   
    if (backgroundProcessorDelay > 0
        //... 
        backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                //执行定时任务
                .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
                        backgroundProcessorDelay, backgroundProcessorDelay,
                        TimeUnit.SECONDS);
    }
}

后台定时检查的任务使用JUC下做定时任务的线程池ScheduledExecutorService.scheduleWithFixedDelay

其中ContainerBackgroundProcessor为定时检查任务,它会从顶级容器开始依次让容器中管理的组件执行backgroundProcess方法

其中Context容器中的Loader组件用于类加载,在backgroundProcess方法中,如果检查到有更新,则会重新加载容器context.reload()

WebappLoader.backgroundProcess

@Override
public void backgroundProcess() {
   
    if (reloadable && modified()) {
   
        try {
   
            //设置线程上下文中的类加载器
            Thread.currentThread().setContextClassLoader
                (WebappLoader.class.getClassLoader());
            if (context != null) {
   
                //重新加载容器
                context.reload();
            }
        } finally {
   
            if (context != null && context.getLoader() != null) {
   
                //结束把类加载器重新设置回来
                Thread.currentThread().setContextClassLoader
                    (context.getLoader().getClassLoader());
            }
        }
    }
}

StandardContext.reload

在Context容器reload方法中,先暂停卸载子组件,再注册启动子组件,在此过程中需要停止接收请求

public synchronized void reload() {
   

    //组件不可用抛出异常
    if (!getState().isAvailable()) {
   
        throw new IllegalStateException
            (sm.getString("standardContext.notStarted", getName()));
    }

    if(log.isInfoEnabled()) {
   
        log.info(sm.getString("standardContext.reloadingStarted",
                getName()));
    }

    //标记 暂停接收请求 
    setPaused(true);

    try {
   
        //停止组件
        stop();
    } catch (LifecycleException e) {
   
        log.error(
            sm.getString("standardContext.stoppingContext", getName()), e);
    }

    try {
   
        //启动组件
        start();
    } catch (LifecycleException e) {
   
        log.error(
            sm.getString("standardContext.startingContext", getName()), e);
    }

    //标记 不再暂停接收请求
    setPaused(false);

    if(log.isInfoEnabled()) {
   
        log.info(sm.getString("standardContext.reloadingCompleted",
                getName()));
    }

}

在stop暂停组件,最终会调用生命周期中的stopInternal去组织停止、销毁容器中使用到的组件

StandardContext.stopInternal

卸载子组件的类前,需要把当前线程的类加载器切换为当时创建的(Loader的类加载器),卸载完又换回来,在这个过程中对应绑定/解绑

组织停止后台线程、子组件、过滤器、管理器、pipeline等容器中使用的组件,最终reset清理context容器

protected synchronized void stopInternal() throws LifecycleException {
   

    //设置停止状态触发事件
    setState(LifecycleState.STOPPING);

    //绑定类加载器(方便卸载子组件)
    ClassLoader oldCCL = bindThread();

    try {
   
        //获取子组件
        final Container[] children = findChildren();

        //停止后台运行线程
        threadStop();

        //停止子组件
        for (Container child : children) {
   
            child.stop();
        }

        //停止过滤器
        filterStop();

        //停止管理器
        Manager manager = getManager();
        if (manager instanceof Lifecycle && ((Lifecycle) manager).getState().isAvailable()) {
   
            ((Lifecycle) manager).stop();
        }

        //停止监听器
        listenerStop();

        //...

        //停止pipeline
        if (pipeline instanceof Lifecycle &&
                ((Lifecycle) pipeline).getState().isAvailable()) {
   
            ((Lifecycle) pipeline).stop();
        }

        //停止其他资源...

    } finally {
   
        //卸载完 解绑,当前线程的类加载器变回原来的
        unbindThread(oldCCL);
    }

    // reset 容器
    context = null;
    try {
   
        resetContext();
    } catch( Exception ex ) {
   
        log.error( "Error resetting context " + this + " " + ex, ex );
    }
}

在卸载类的过程中,会使用当前context容器下的类加载器去进行卸载

后续start启动再新创建context容器中使用到的组件,其中类加载器流程总结如下:

WebappClassLoaderBase.loadClass

  1. 检查类是否加载

  2. 拿到扩展类加载器调用(先引导、再扩展,防止核心类库被破坏)

    • javaseLoader = getJavaseClassLoader()

    • javaseLoader.loadClass(name) (这里扩展类交给引导类进行加载,还是以前双亲委派模型代码)

  3. 当前类加载器尝试类加载 findClass(name)(这里可能交给父类加载,比如之前说过的共享的SharedClassLoader)

  4. 应用类加载器尝试加载 Class.forName(name, false, parent)

  5. 抛出异常 throw new ClassNotFoundException(name)

实际上Tomcat就是把当前类加载器尝试加载的时机放到应用类加载器前,还是引导、扩展类加载优化加载(防止核心类库被破坏)

总结

双亲委派模型优先将类交给父类加载,如果父类不能加载再由自己加载,当自己也无法加载时抛出ClassNotFoundException异常,能够保证核心类库不被破坏

通过类加载器可以解决隔离的问题,判断类是否相同时要满足全限定类名和类加载器都相同

Tomcat为了解决多Web应用间类的隔离,自定义WebAppClassLoader类加载器作为Context容器的Loader

WebAppClassLoader类加载流程先检查类加载,优先使用引导、扩展类加载器,再尝试自己的父类/自己进行加载,最后在尝试让应用类加载器加载,都无法加载抛出异常

🌠最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Tomcat全解析:架构设计与核心组件实现,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

相关文章
|
缓存 Java 应用服务中间件
Tomcat是如何打破"双亲委派"机制的?
上文我们详细了解了类加载以及什么是双亲委派机制,相信很多童鞋都了解Tomcat打破了双亲委派机制,本文将对Tomcat为什么要打破双亲委派机制,以及Tomcat是如何打破双亲委派机制的,进行完整性的复盘与解析。
3610 0
Tomcat是如何打破"双亲委派"机制的?
|
11月前
|
Java 应用服务中间件
面对海量网络请求,Tomcat线程池如何进行扩展?
【10月更文挑战第4天】本文详细探讨了Tomcat线程池相较于标准Java实用工具包(JUC)线程池的关键改进。首先,Tomcat线程池在启动时即预先创建全部核心线程,以应对启动初期的高并发请求。其次,通过重写阻塞队列的入队逻辑,Tomcat能够在任务数超过当前线程数但未达最大线程数时,及时创建非核心线程,而非等到队列满才行动。此外,Tomcat还引入了在拒绝策略触发后重新尝试入队的机制,以提高吞吐量。这些优化使得Tomcat线程池更适应IO密集型任务,有效提升了性能。
面对海量网络请求,Tomcat线程池如何进行扩展?
|
11月前
|
消息中间件 存储 Java
RocketMQ(一):消息中间件缘起,一览整体架构及核心组件
【10月更文挑战第15天】本文介绍了消息中间件的基本概念和特点,重点解析了RocketMQ的整体架构和核心组件。消息中间件如RocketMQ、RabbitMQ、Kafka等,具备异步通信、持久化、削峰填谷、系统解耦等特点,适用于分布式系统。RocketMQ的架构包括NameServer、Broker、Producer、Consumer等组件,通过这些组件实现消息的生产、存储和消费。文章还提供了Spring Boot快速上手RocketMQ的示例代码,帮助读者快速入门。
|
11月前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
11月前
|
NoSQL 关系型数据库 Java
以超卖为例✨各种场景下如何防止并发污染数据?
【10月更文挑战第8天】本文以商品库存扣减为例,探讨了在各种场景下如何防止并发操作导致的数据不一致问题。文章首先介绍了悲观锁和乐观锁的概念,然后分别从Java层面和中间件层面详细讲解了多种解决方案,包括使用synchronized、ReentrantLock、乐观锁(CAS)、数据库乐观锁和悲观锁、分布式锁(如Redis锁)等。最后,针对高并发场景,提出了将数据预热到Redis并使用Lua脚本保证原子性的方法。通过这些方法,可以有效防止超卖等数据污染问题。
以超卖为例✨各种场景下如何防止并发污染数据?
|
11月前
|
网络协议 Java 应用服务中间件
Tomcat中的WebSocket是如何实现的?
【10月更文挑战第7天】本文介绍了WebSocket在Tomcat中的实现,包括其全双工通信、单个TCP连接、协议升级和事件驱动的特点。通过Spring Boot项目整合WebSocket,展示了如何配置依赖、创建WebSocket处理类和配置类。详细解析了WebSocket的原理,包括ServerEndpointExporter的注册过程和请求处理流程。总结了WebSocket与HTTP请求处理的区别,并提供了进一步学习的资源。
Tomcat中的WebSocket是如何实现的?
|
11月前
|
网络协议 Java 应用服务中间件
深入浅出Tomcat网络通信的高并发处理机制
【10月更文挑战第3天】本文详细解析了Tomcat在处理高并发网络请求时的机制,重点关注了其三种不同的IO模型:NioEndPoint、Nio2EndPoint 和 AprEndPoint。NioEndPoint 采用多路复用模型,通过 Acceptor 接收连接、Poller 监听事件及 Executor 处理请求;Nio2EndPoint 则使用 AIO 异步模型,通过回调函数处理连接和数据就绪事件;AprEndPoint 通过 JNI 调用本地库实现高性能,但已在 Tomcat 10 中弃用
深入浅出Tomcat网络通信的高并发处理机制
|
11月前
|
存储 缓存 安全
ReflectionUtils提高反射性能!
【10月更文挑战第14天】本文介绍了如何使用Spring框架的`ReflectionUtils`工具类来提高反射操作的性能。通过对比原生反射API与`ReflectionUtils`在创建实例、调用方法和设置字段值等方面的性能,展示了`ReflectionUtils`在多次调用时显著的性能优势。文章还深入分析了`ReflectionUtils`的源码,揭示了其通过缓存机制和减少对象创建次数来提升性能的秘密。最后,作者建议在项目中遇到反射需求时优先考虑使用`ReflectionUtils`。
|
11月前
|
设计模式 算法 Java
盘点Tomcat中常见的13种设计模式
【10月更文挑战第6天】本文深入探讨了Tomcat中常见的13种设计模式,包括单例模式、工厂模式、适配器模式、装饰者模式、组合模式、外观模式、享元模式、责任链模式、命令模式、迭代器模式、观察者模式、策略模式和模板方法模式。通过具体示例,展示了这些设计模式如何协同工作,支撑起Tomcat的高性能和高灵活性。
|
11月前
|
存储 Java 开发者
丸辣!BigDecimal又踩坑了
【10月更文挑战第13天】本文介绍了在Java中使用BigDecimal进行高精度计算时容易遇到的几个坑,并给出了相应的解决方案。文章通过具体的示例详细讲解了BigDecimal在创建实例、toString方法、比较大小、运算以及计算价格时的常见问题和最佳实践。适合需要进行金融计算的开发者阅读。