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

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

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

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

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

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

双亲委派模型

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

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

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

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

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

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

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

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

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

ClassLoader.loadClass

java 复制代码
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 复制代码
错误: 在类 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

java 复制代码
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

java 复制代码
@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方法中,先暂停卸载子组件,再注册启动子组件,在此过程中需要停止接收请求

java 复制代码
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容器

java 复制代码
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持续关注喔~

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

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

相关推荐
qq_4419960530 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼37 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田3 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉5 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v5 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge5 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@5 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄5 小时前
SpringBoot
java·spring