首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一文讲透 Tomcat 的类加载机制!揭秘类加载核心

一文讲透 Tomcat 的类加载机制!揭秘类加载核心

作者头像
玄姐谈AGI
发布于 2021-01-11 06:26:55
发布于 2021-01-11 06:26:55
2.5K0
举报
文章被收录于专栏:架构之美架构之美

- 前言 -

你了解 Apache Tomcat 的类加载机制吗?本文将从底层原理切入,彻底揭秘 Tomcat 类加载所涉及的源码、机制和方案,助你深入掌握 Tomcat 类加载核心!

在啃这篇干货的同时,【文章结尾处】还有玄姐送你的【免费福利】在等你,记得查收哦!

- JVM 类加载器 -

1、JVM类加载器

说起 Tomcat 类加载器,就不得不先简单说一下 JVM 类加载器,如下图所示:

  • 启动类加载器:Bootstrap ClassLoader,用于加载JVM提供的基础运行类,即位于%JAVA_HOME%/jre/lib目录下的核 心类库;
  • 扩展类加载器:Extension ClassLoader, Java提供的一个标准的扩展机制用于加载除核心类库外的Jar包,即只要复制 到指定的扩展目录(可以多个)下的Jar, JVM会自动加载(不需要通过-classpath指定)。默认的扩展目录是%JAVA_HOME%加e/lib/ext。典型的应用场景就是,Java使用该类加载 器加载JVM默认提供的但是不属于核心类库的Jar。不推荐将应用程序依赖的 类库放置到扩展目录下,因为该目录下的类库对所有基于该JVM运行的应用程序可见;
  • 应用程序类加载器:Application ClassLoader ,用于加载环境变量CLASSPATH (不推荐使用)指定目录下的或者-classpath运行 参数指定的Jar包。System类加载器通常用于加载应用程序Jar包及其启动入口类(Tomcat 的Bootstrap类即由System类加载器加载)。

这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。

双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。

这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

这里请注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。

2、类加载器的源码

代码语言:javascript
AI代码解释
复制
public abstract class ClassLoader {  //  每个类加载器都有一个父加载器  private final ClassLoader parent;  public Class<?> loadClass(String name) throws ClassNotFoundException {        return loadClass(name, false);    }     protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException    {            // First, check if the class has already been loaded            Class<?> c = findLoadedClass(name);           // 如果没有加载过            if (c == null) {                if (parent != null) {                  //  先委托给父加载器去加载,注意这是个递归调用                 c = parent.loadClass(name, false);                } else {                 // 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了                   c = findBootstrapClassOrNull(name);                }                          // 如果父加载器没加载成功,调用自己的 findClass 去加载                if (c == null) {                            c = findClass(name);                }            }                     return c;        }            }    //ClassLoader 中findClass方式需要被子类覆盖,下面这段代码就是对应代码      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){        }    }

我们自定义类加载器就需要重写ClassLoader的loadClass方法。

- Tomcat 的类加载机制 -

1、加载机制的特点

  • 隔离性:Web应用类库相互隔离,避免依赖库或者应用包相互影响。设想一下,如果我们 有两个Web应用,一个釆用了Spring 2.5, 一个采用了Spring 4.0,而应用服务器使用一个 类加载器加载,那么Web应用将会由于Jar包覆盖而导致无法启动成功;
  • 灵活性:既然Web应用之间的类加载器相互独立,那么我们就能只针对一个Web应用进行 重新部署,此时该Web应用的类加载器将会重新创建,而且不会影响其他Web应用。如果 釆用一个类加载器,显然无法实现,因为只有一个类加载器的时候,类之间的依赖是杂 乱无章的,无法完整地移除某个Web应用的类;
  • 性能:由于每个Web应用都有一个类加载器,因此Web应用在加载类时,不会搜索其他 Web应用包含的Jar包,性能自然高于应用服务器只有一个类加载器的情况。

2、Tomcat 的类加载方案

  • 引导类加载器 和 扩展类加载器 的作⽤不变;
  • 系统类加载器正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使⽤该变量,⽽是加载tomcat启动的类,⽐如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下;
  • Common 通⽤类加载器加载Tomcat使⽤以及应⽤通⽤的⼀些类,位于CATALINA_HOME/lib下,⽐如servlet-api.jar;
  • Catalina ClassLoader ⽤于加载服务器内部可⻅类,这些类应⽤程序不能访问;
  • SharedClassLoader ⽤于加载应⽤程序共享类,这些类服务器不会依赖;
  • WebappClassLoader,每个应⽤程序都会有⼀个独⼀⽆⼆的Webapp ClassLoader,他⽤来加载本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。

tomcat 8.5 默认改变了严格的双亲委派机制:

  • 从缓存中加载;
  • 如果缓存中没有,会先调用ExtClassLoader进行加载, 扩展类加载器是遵循双亲委派的,他会调用bootstrap,查看对应的lib有没有,然后回退给ExtClassLoader对扩展包下的数据进行加载;
  • 如果未加载到,则从 /WEB-INF/classes加载;
  • 如果未加载到,则从 /WEB-INF/lib/*.jar 加载如果未加载到,WebAppclassLoader 会委派给SharedClassLoader,SharedClassLoad会委派给CommonClassLoader.....,依次委派给BootstrapClassLoader, 然后BootstrapClassLoader 在自己目录中查找对应的类如果有则进行加载,如果没有他会委派给下一级ExtClassLoader,ExtClassLoader再查找自己目录下的类,如果有则加载如果没有则委派给下一级……遵循双亲委派原则。

3、分析应用类加载器的加载过程

应用类加载器为WebappClassLoader ,他的loadClass在他的父类WebappClassLoaderBase中。

代码语言:javascript
AI代码解释
复制
  public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {        synchronized (getClassLoadingLock(name)) {            if (log.isDebugEnabled())                log.debug("loadClass(" + name + ", " + resolve + ")");            Class<?> clazz = null;            // Log access to stopped class loader            checkStateForClassLoading(name);                //从当前ClassLoader的本地缓存中加载类,如果找到则返回            clazz = findLoadedClass0(name);            if (clazz != null) {                if (log.isDebugEnabled())                    log.debug("  Returning class from cache");                if (resolve)                    resolveClass(clazz);                return clazz;            }            // 本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。            clazz = findLoadedClass(name);            if (clazz != null) {                if (log.isDebugEnabled())                    log.debug("  Returning class from cache");                if (resolve)                    resolveClass(clazz);                return clazz;            }            String resourceName = binaryNameToPath(name, false);            //此时的javaseClassLoader是扩展类加载器  是把扩展类加载器赋值给了javaseClassLoader            ClassLoader javaseLoader = getJavaseClassLoader();            boolean tryLoadingFromJavaseLoader;            try {              .....            //如果可以用getResource得到            //如果能用扩展类加载器的getResource得到就证明可以被扩展类加载器加载到接下来安排扩展类加载器加载            if (tryLoadingFromJavaseLoader) {                try {                    //使用扩展类加载器进行加载                    clazz = javaseLoader.loadClass(name);                    if (clazz != null) {                        if (resolve)                            resolveClass(clazz);                        return clazz;                    }                } catch (ClassNotFoundException e) {                    // Ignore                }            }            // (0.5) Permission to access this class when using a SecurityManager            if (securityManager != null) {                int i = name.lastIndexOf('.');                if (i >= 0) {                    try {                        securityManager.checkPackageAccess(name.substring(0,i));                    } catch (SecurityException se) {                        String error = "Security Violation, attempt to use " +                            "Restricted Class: " + name;                        log.info(error, se);                        throw new ClassNotFoundException(error, se);                    }                }            }            boolean delegateLoad = delegate || filter(name, true);            // (1) Delegate to our parent if requested            //如果是true就是用父类加载器进行加载            if (delegateLoad) {                if (log.isDebugEnabled())                    log.debug("  Delegating to parent classloader1 " + parent);                try {                    clazz = Class.forName(name, false, parent);                    if (clazz != null) {                        if (log.isDebugEnabled())                            log.debug("  Loading class from parent");                        if (resolve)                            resolveClass(clazz);                        return clazz;                    }                } catch (ClassNotFoundException e) {                    // Ignore                }            }            // (2) Search local repositories            if (log.isDebugEnabled())                log.debug("  Searching local repositories");            try {                // 本地进行加载                clazz = findClass(name);                if (clazz != null) {                    if (log.isDebugEnabled())                        log.debug("  Loading class from local repository");                    if (resolve)                        resolveClass(clazz);                    return clazz;                }            } catch (ClassNotFoundException e) {                // Ignore            }            // (3) Delegate to parent unconditionally            //到这里还是没有加载上再次尝试使用父类加载器进行加载            if (!delegateLoad) {                    if (log.isDebugEnabled())                    log.debug("  Delegating to parent classloader at end: " + parent);                try {                    clazz = Class.forName(name, false, parent);                    if (clazz != null) {                        if (log.isDebugEnabled())                            log.debug("  Loading class from parent");                        if (resolve)                            resolveClass(clazz);                        return clazz;                    }                } catch (ClassNotFoundException e) {                    // Ignore                }            }        }        throw new ClassNotFoundException(name);    }

注:在37行英文注释中标注获取的是系统类加载器,但我们debug的时候会发现他是扩展类加载器,实际中我们可以推断出他应该是扩展类加载器,因为如果我们加载的类在扩展类加载器路径下已经存在的话,那我们直接调用系统类加载器是就是错误的了,下图为debug后获取的类加载器的验证。

总结:tomcat打破了双亲委派的原则,实际是在应用类加载器中打破了双亲委派,其他类加载器还是遵循双亲委派的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构之美 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
javascript异步编程之generator(生成器函数)与asnyc/await语法糖
相比于传统回调函数的方式处理异步调用,Promise最大的优势就是可以链式调用解决回调嵌套的问题。但是这样写依然会有大量的回调函数,虽然他们之间没有嵌套,但是还是没有达到传统同步代码的可读性。如果以下面的方式写异步代码,它是很简洁,也更容易阅读的。
开水泡饭
2022/12/26
4460
面试官: 说说你对async的理解
async是generator和promise的语法糖,利用迭代器的状态机和promise来进行自更新!
用户8200753
2023/10/22
2950
async/await 源码实现
如果你有一个这样的场景,b依赖于a,c依赖于b,那么我们只能通过promise then的方式实现。这样的的可读性就会变得很差,而且不利于流程控制,比如我想在某个条件下只走到 b 就不往下执行 c 了,这种时候就变得不是很好控制!
用户4131414
2020/03/19
1.4K0
字节前端高频手写面试题(持续更新中)1
观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
helloworld1024
2023/01/03
8440
Promise/async/Generator实现原理解析
笔者刚接触async/await时,就被其暂停执行的特性吸引了,心想在没有原生API支持的情况下,await居然能挂起当前方法,实现暂停执行,我感到十分好奇。好奇心驱使我一层一层剥开有关JS异步编程的一切。阅读完本文,读者应该能够了解:
Nealyang
2020/03/25
2K0
async/await 原理及执行顺序分析
之前写了篇文章《这一次,彻底理解Promise原理》,剖析了Promise的相关原理。我们都知道,Promise解决了回调地狱的问题,但是如果遇到复杂的业务,代码里面会包含大量的 then 函数,使得代码依然不是太容易阅读。
桃翁
2019/11/08
2.1K0
JavaScript 的 async/await : async 和 await 在干什么
async 是“异步”的简写,而 await 可以认为是 async wait 的简写。 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。 await 只能出现在 async 函数中。
一个会写诗的程序员
2018/08/17
1.1K0
JavaScript 的 async/await : async 和 await 在干什么
如何写出一个惊艳面试官的 Promise【近 1W字】 前言源码1.Promise2.Generator3.async 和 await4.Pro
1.高级 WEB 面试会让你手写一个Promise,Generator 的 PolyFill(一段代码); 2.在写之前我们简单回顾下他们的作用; 3.手写模块见PolyFill.
火狼1
2020/05/09
8270
如何写出一个惊艳面试官的 Promise【近 1W字】
                            前言源码1.Promise2.Generator3.async 和 await4.Pro
async与await的原理揭秘
async和await是es7语法,在babel中会被转译,我们看一下专一前和转译后的源码:
挥刀北上
2021/12/10
9300
async与await的原理揭秘
async/await剖析
JavaScript是单线程的,为了避免同步阻塞可能会带来的一些负面影响,引入了异步非阻塞机制,而对于异步执行的解决方案从最早的回调函数,到ES6的Promise对象以及Generator函数,每次都有所改进,但是却又美中不足,他们都有额外的复杂性,都需要理解抽象的底层运行机制,直到在ES7中引入了async/await,他可以简化使用多个Promise时的同步行为,在编程的时候甚至都不需要关心这个操作是否为异步操作。
WindRunnerMax
2020/08/27
3910
理解 ES6 generator
与 promise 对象类似,这里运用鸭子模型进行判断,如果对象中有 next 与 throw 两个方法,那么就认为这个对象是一个生成器对象。
翼德张
2022/01/13
3240
Promise 向左,Async/Await 向右?
1. 前言 从事前端开发至今,异步问题经历了 Callback Hell 的绝望,Promise/Deffered 的规范混战,到 Generator 的所向披靡,到如今 Async/Await 为大众所接受,这其中 Promise 和 Async/Await 依然活跃代码中,对他们的认识和评价也经历多次反转,也有各自的拥趸,形成了一直延续至今的爱恨情仇,其背后的思考和启发,依旧值得我们深思。 预先声明: 本文的目标并不是引发大家的论战,也不想去推崇其中任何一种方式来作为前端异步的唯一最佳实践,想在介绍下
用户1097444
2022/06/29
6250
Promise 向左,Async/Await 向右?
面试官问 async、await 函数原理是在问什么?
这周看的是 co 的源码,我对 co 比较陌生,没有了解和使用过。因此在看源码之前,我希望能大概了解 co 是什么,解决了什么问题。
若川
2021/09/27
7770
Generator函数
JavaScript是单线程的,异步编程对于 JavaScript语言非常重要。如果没有异步编程,根本没法用,得卡死不可。
木子星兮
2020/07/16
1.2K0
ES6读书笔记(三)
前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》,《ES6读书笔记(二)》,现在为第三篇,本篇内容包括:
全栈程序员站长
2021/07/06
1.3K0
【ECMAScript6】es6 要点(二)Promise | 自个写一个Promise | Generator | Async/Await
但是,我们不能无限制地调用next从Generator实例中获取值。否则最后会返回undefined。原因:Generator犹如一种序列,一旦序列中的值被消费,你就不能再次消费它。即,序列为空后,再次调用就会返回undefined!。
前端修罗场
2023/10/07
5150
【ECMAScript6】es6 要点(二)Promise | 自个写一个Promise | Generator | Async/Await
手写async,await 理解内部原理
async await 底层并不是新东西,只是用起来比Generator函数更舒服的api...
心念
2023/01/11
8990
原生JS灵魂之问(下), 冲刺进阶最后一公里(附个人成长经验分享)
笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,一共分三次发,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。这是本系列的第三篇。
用户4131414
2020/03/19
2.2K0
co 函数库的含义和用法
======================================== 以下是《深入掌握 ECMAScript 6 异步编程》系列文章的第三篇。 Generator函数的含义与用法 Th
ruanyf
2018/04/12
1.1K0
co 函数库的含义和用法
前端异步代码解决方案实践(二)
早前有针对 Promise 的语法写过博文,不过仅限入门级别,浅尝辄止食而无味。后面一直想写 Promise 实现,碍于理解程度有限,多次下笔未能满意。一拖再拖,时至今日。
前朝楚水
2018/07/26
3.6K0
推荐阅读
相关推荐
javascript异步编程之generator(生成器函数)与asnyc/await语法糖
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
首页
学习
活动
专区
圈层
工具
MCP广场
首页
学习
活动
专区
圈层
工具
MCP广场