← 返回题目列表

Tomcat 的类加载机制是什么?为什么要打破双亲委派?

高频 困难 第 13 / 23 题 更新于 2026/07/25
Tomcat类加载双亲委派WebappClassLoader

简化版

Tomcat 为每个 Web 应用创建一个独立的 WebappClassLoader,让不同应用能使用不同版本的依赖、互不干扰,并支持应用卸载时回收类(热部署基础)。它打破了标准双亲委派:加载应用自己的类时,优先从本应用的 WEB-INF/classesWEB-INF/lib 加载(而不是先委托父加载器),从而实现应用隔离。但它仍保护 JDK 核心类和 Servlet API——这些不允许应用覆盖,所以是「有选择地」打破,不是完全推翻。

详细版

标准双亲委派 vs Tomcat 的破坏:

标准双亲委派Tomcat WebappClassLoader
加载顺序先委托父加载器,父加载不了才自己加载先自己(本应用)加载,特定类才委托父
目的保证核心类唯一、安全应用隔离 + 热部署

Tomcat 的类加载器层级(自上而下):

Bootstrap ClassLoader         (JVM 核心类 rt.jar / JDK 模块)

System/Platform ClassLoader   (JDK 扩展、CLASSPATH)

Common ClassLoader            (Tomcat 与所有 Web 应用共享的类,如 lib/)

WebappClassLoader(每个应用一个)  ← 加载本应用 WEB-INF/classes 和 WEB-INF/lib

WebappClassLoader 的加载逻辑(打破双亲委派处):

  1. 先查已加载缓存
  2. 再让 Bootstrap 加载 JDK 核心类(这部分不破坏,保证安全)。
  3. 然后优先从本应用 WEB-INF/classesWEB-INF/lib 加载(这里破坏了双亲委派——应用类优先自己加载)。
  4. 本应用找不到,才委托 Common 等父加载器

完整版教学

一、先回顾:双亲委派原本解决什么

标准的双亲委派模型:一个类加载器收到加载请求,先委托给父加载器去加载,父加载器再往上委托,直到 Bootstrap;只有父加载器加载不了,子加载器才自己加载。

它的目的:

  • 保证核心类的唯一性和安全:像 java.lang.String 这种核心类,永远由 Bootstrap 加载,不可能被应用自定义的同名类替换(防止恶意代码用假的 String 替换真的)。
  • 避免重复加载:同一个类只被加载一次,由最顶层能加载它的加载器负责。

这在普通 Java 应用里很好。但 Web 容器有特殊需求,标准模型满足不了。

二、Web 容器为什么需要打破它:应用隔离

一个 Tomcat 可以同时部署多个 Web 应用(多个 war)。这带来一个核心矛盾:

A 应用依赖 Jackson 2.13,B 应用依赖 Jackson 2.17。如果按双亲委派,两个应用的 Jackson 类都由公共父加载器加载,那就只能有一个版本——两个应用没法各用各的版本,冲突。

要让每个应用拥有自己独立的依赖版本、互不污染,就必须让每个应用有自己的类加载器,且加载应用类时优先加载本应用的(而不是先去公共父加载器找)。这就必然要打破双亲委派——「应用类优先自己加载」正是对「先委托父加载器」的破坏。

此外,热部署也需要它:每个应用一个独立 ClassLoader,卸载应用时把这个 ClassLoader 连同它加载的类一起丢弃回收,就能实现「不重启 Tomcat 单独重新部署一个应用」。

三、Tomcat 的类加载器层级

Tomcat 设计了多层类加载器(不同版本细节略有差异,核心思想一致):

  • Bootstrap:加载 JDK 核心类。
  • System/Platform:加载 JDK 扩展、系统 CLASSPATH。
  • Common ClassLoader:加载 Tomcat 自身和所有 Web 应用共享的类(Tomcat 的 lib/ 目录下的 jar)。放这里的类是全局共享的。
  • WebappClassLoader每个 Web 应用一个,加载该应用的 WEB-INF/classes(类文件)和 WEB-INF/lib(jar)。这是实现应用隔离的关键——每个应用一个,互相看不见对方的类。
类加载器典型加载内容是否应用独享面试要点
BootstrapJDK 核心类保护核心类唯一性
System/PlatformJDK 平台类、系统类路径普通 Java 应用常见父加载器
CommonTomcat lib/ 和共享类否,所有 Web 应用共享放错业务依赖会污染全部应用
WebappClassLoaderWEB-INF/classesWEB-INF/lib是,每个应用一个应用隔离和热部署的关键
Tomcat
  Common: servlet-api.jar, tomcat 自身类
  AppA WebappClassLoader: WEB-INF/lib/jackson-2.13.jar
  AppB WebappClassLoader: WEB-INF/lib/jackson-2.17.jar

上面这个例子里,A 和 B 的 Jackson 类名可能相同,但由不同 WebappClassLoader 加载,因此互不覆盖。若把某个 Jackson 放到 Tomcat lib/,它会变成 Common 层共享依赖,隔离就被破坏。

记忆钩子:Tomcat 不是为了“叛逆”才破坏双亲委派,而是为了让每个 Web 应用拥有自己的依赖小世界。

四、到底「打破」的是哪一段(关键细节)

面试爱追问「Tomcat 是不是完全不用双亲委派了」——不是。它打破的只是「应用自己的类」这一段

WebappClassLoader 加载类的顺序:

  1. 先查缓存(已加载过的直接返回)。
  2. 让 Bootstrap 尝试加载 JDK 核心类java.* 等)——这一步不破坏双亲委派,核心类仍由 Bootstrap 保证唯一和安全,应用无法覆盖 java.lang.String 之类。
  3. 然后优先在本应用的 WEB-INF/classesWEB-INF/lib 里找——这里破坏了双亲委派(应用类优先自己加载,而非先委托 Common)。
  4. 本应用找不到,才向上委托 Common 等父加载器

所以准确说法是:JDK 核心类、Jakarta Servlet API 等容器关键类仍受保护(走双亲委派),只有应用自己的业务类是「本应用优先」(打破双亲委派)。 这是「有选择的打破」,兼顾了安全和隔离。

五、隔离带来的好处与热部署

这套机制的收益:

  • 依赖隔离:A、B 应用各用各的依赖版本,一个应用升级库不影响另一个,互不污染。
  • 热部署基础:每个应用独立 ClassLoader。卸载应用时,理论上它的 WebappClassLoader 和加载的所有类都能被 GC 回收,从而可以单独重新部署一个应用而不重启整个 Tomcat

六、常见坑:内存泄漏与类冲突

① 依赖放错位置污染全局:把某个应用的依赖 jar 放到 Tomcat 的 lib/(Common 加载),会让它变成全局共享,可能污染其他应用(其他应用被迫用这个版本)。应用依赖应放 WEB-INF/lib

② WebappClassLoader 内存泄漏(高频):热部署/卸载应用时,如果应用里有东西长期持有 WebappClassLoader 的引用,这个 ClassLoader 和它加载的所有类就无法被 GC,反复重新部署会导致 Metaspace/PermGen OOM。常见「泄漏源」:

  • ThreadLocal 没清理:线程池的线程是 Tomcat 共享的,如果应用往 ThreadLocal 塞了自己的类实例又没 remove,线程持有它 → 间接持有 WebappClassLoader。
  • 应用启动的线程/定时器没关闭:这些线程的上下文类加载器指向 WebappClassLoader。
  • 注册了 JDBC Driver 没注销DriverManager 是全局的,持有应用的 Driver 类。

这些是 Tomcat 热部署内存泄漏的经典原因,理解类加载隔离才能定位。

③ 类冲突排查:遇到 ClassCastExceptionNoSuchMethodErrorLinkageError,先确认类到底从哪个 jar / 哪个 ClassLoader 加载。关键认知:两个「同名同全限定名」的类,若由不同 ClassLoader 加载,在 JVM 看来是完全不同的两个类型——把一个赋给另一个就会 ClassCastException。这解释了很多诡异的类型转换错误。

七、常见误区与追问

  • 误区:Tomcat 完全抛弃了双亲委派。 它是选择性打破,JDK 核心类和容器关键类仍优先由父加载器保护,应用类才本应用优先。
  • 误区:所有依赖放到 Tomcat lib/ 更省事。 这样会让依赖全局共享,容易让多个应用发生版本污染,业务依赖应放在各自 WEB-INF/lib
  • 追问:为什么每个 Web 应用要独立 ClassLoader? 为了依赖版本隔离和热部署,卸载应用时可以回收该应用加载的类。
  • 追问:同名类为什么还会 ClassCastException JVM 判断类型时看“类全限定名 + 类加载器”,不同加载器加载的同名类不是同一个类型。
  • 误区:热部署内存泄漏只和堆对象有关。 WebappClassLoader 被线程、ThreadLocal、JDBC Driver 等持有时,会连同类元数据一起留在 Metaspace。
  • 追问:ThreadLocal 为什么会导致类加载器泄漏? Tomcat Worker 线程可能比应用活得更久,ThreadLocal 值若引用应用类对象,就会间接持有 WebappClassLoader。

八、加强记忆

Tomcat 为每个 Web 应用一个 WebappClassLoader,实现依赖隔离(不同应用用不同版本,互不污染)和热部署(卸载时回收 ClassLoader)。加载器层级:Bootstrap → System → Common(Tomcat 与所有应用共享,lib/)→ WebappClassLoader(每应用一个,加载 WEB-INF/classesWEB-INF/lib打破双亲委派体现在:加载应用自己的类时「本应用优先」(先查 WEB-INF,找不到才委托父),但 JDK 核心类和 Servlet API 仍走双亲委派受保护,不允许覆盖——是「有选择的打破」。坑:应用依赖放 lib/ 会污染全局ThreadLocal/线程/JDBC Driver 未清理会持有 WebappClassLoader 导致卸载后 Metaspace 泄漏不同 ClassLoader 加载的同名类是不同类型,会 ClassCastException