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 应用服务中间件
《深入理解java虚拟机》——Tomcat类加载器体系结构
《深入理解java虚拟机》——Tomcat类加载器体系结构
|
前端开发 安全 Java
类加载器原理
一、类加载 二、链接 三、初始化
类加载器原理
|
4月前
|
前端开发 Java 应用服务中间件
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
本文详细介绍了Java类加载机制及其在Tomcat中的应用。首先回顾了Java默认的类加载器,包括启动类加载器、扩展类加载器和应用程序类加载器,并解释了双亲委派模型的工作原理及其重要性。接着,文章分析了Tomcat为何不能使用默认类加载机制,因为它需要解决多个应用程序共存时的类库版本冲突、资源共享、类库隔离及JSP文件热更新等问题。最后,详细展示了Tomcat独特的类加载器设计,包括Common、Catalina、Shared、WebApp和Jsp类加载器,确保了系统的稳定性和安全性。通过这种设计,Tomcat实现了不同应用程序间的类库隔离与共享,同时支持JSP文件的热插拔。
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
|
5月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
186 0
|
8月前
|
监控 安全 Java
JVM工作原理与实战(九):类加载器-启动类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了启动类加载器、通过启动类加载器去加载用户jar包等内容。
91 8
|
8月前
|
缓存 前端开发 安全
细究Java类加载机制和Tomcat类加载机制
细究Java类加载机制和Tomcat类加载机制
62 0
|
前端开发 Java 数据库连接
【面试题精讲】JVM-类加载器-扩展类加载器
【面试题精讲】JVM-类加载器-扩展类加载器
|
存储 Java 应用服务中间件
|
XML 前端开发 JavaScript
Tomcat - 都说Tomcat违背了双亲委派机制,到底对不对?
Tomcat - 都说Tomcat违背了双亲委派机制,到底对不对?
167 0
|
缓存 Java 应用服务中间件
类加载器系列(二)——从源码角度理解双亲委派模型
类加载器系列(二)——从源码角度理解双亲委派模型
217 0
类加载器系列(二)——从源码角度理解双亲委派模型