4. 类加载机制:类加载器层次结构、双亲委派模型、类卸载条件

在数据库迁移版本升级场景中,Metaspace 区域的内存泄漏或频繁 GC 停顿,其根源往往与类加载器的生命周期、类的卸载条件密切相关。理解 JVM 的类加载机制,是定位 Metaspace 问题的第一把钥匙。

4.1 类加载器层次结构

JVM 在启动时预定义了三个核心类加载器,它们构成了严格的父子层级关系,而非继承关系。每个加载器负责加载特定路径下的类。

类加载器 加载路径 加载时机 关键特征
Bootstrap ClassLoader rt.jar、java.base 等核心库 JVM 启动时 C++ 实现,无对应的 Java 对象;是顶层加载器
Extension ClassLoader jre/lib/ext 或 java.ext.dirs JVM 启动后 Java 实现,父加载器为 Bootstrap
Application ClassLoader classpath(用户类路径) 加载用户主类时 Java 实现,父加载器为 Extension;默认上下文加载器
升级场景提示: 数据库驱动、连接池(如 HikariCP、Druid)以及 ORM 框架(如 MyBatis、Hibernate)的类通常由 Application ClassLoader 加载。如果这些框架在升级后引入了新的类加载器(如 Tomcat 的 WebappClassLoader),会导致 Metaspace 中类元数据成倍增长。
类加载器层次结构 类加载器层次结构(双亲委派模型) Bootstrap ClassLoader (C++ / 核心库) Extension ClassLoader (ext 目录) Application ClassLoader (classpath) 自定义 ClassLoader (Tomcat / OSGi / 热部署) 委派方向 1. 子加载器收到请求 2. 向上委派给父加载器 3. 父加载器无法加载时 子加载器自行尝试 加载顺序 Bootstrap → Extension → Application → 自定义

4.2 双亲委派模型

双亲委派模型是 Java 类加载的核心安全机制。当一个类加载器收到加载请求时,它不会立即自己加载,而是先将请求委派给父加载器处理,只有父加载器无法加载时,子加载器才会尝试自己加载。

工作流程

  1. 检查已加载: 当前加载器检查该类是否已被自己加载过。
  2. 向上委派: 若未加载,则调用父加载器的 loadClass() 方法。
  3. 递归委派: 父加载器重复步骤 1-2,直到 Bootstrap ClassLoader。
  4. 向下尝试: 若顶层父加载器无法加载(抛出 ClassNotFoundException),则子加载器调用 findClass() 自行加载。
升级场景关联: 数据库迁移升级后,如果新版本的 JDBC 驱动 jar 包被多个类加载器(如应用加载器和 Tomcat 公共加载器)同时加载,会导致 Metaspace 中出现多份相同的类元数据。双亲委派模型可以避免核心类被重复加载,但无法阻止不同类加载器加载同一份 jar 包。

4.3 类卸载条件

类的卸载是 Metaspace 回收的关键。一个类被卸载需要同时满足三个严苛条件,这些条件在数据库升级场景中经常被打破。

条件编号 条件描述 升级场景中的典型破坏原因
条件 1 该类的所有实例都已被 GC 回收 连接池对象、ORM 会话工厂对象未被及时关闭
条件 2 加载该类的 ClassLoader 已被 GC 回收 热部署或框架升级后,旧 ClassLoader 仍被线程上下文引用
条件 3 该类对应的 java.lang.Class 对象没有任何地方被引用 静态集合、缓存、ThreadLocal 中持有 Class 引用
关键结论: 三个条件缺一不可。在数据库迁移升级后,最常见的 Metaspace 泄漏原因是 条件 2 和条件 3 被破坏——旧类加载器被线程上下文引用,或者 Class 对象被静态缓存持有,导致类元数据无法卸载,Metaspace 持续膨胀。
类卸载条件与 Metaspace 泄漏路径 类卸载条件与 Metaspace 泄漏路径 条件 1:实例已回收 所有对象实例不可达 条件 2:ClassLoader 已回收 加载器不可达 条件 3:Class 对象无引用 无强引用指向 Class AND 类卸载成功 Metaspace 泄漏 常见泄漏原因 • 线程上下文持有旧 ClassLoader • 静态 Map 缓存 Class 对象 • ThreadLocal 未清理 • 反射生成的代理类

4.4 升级场景中的类加载陷阱

  • 多版本 jar 共存: 数据库驱动升级后,旧版本 jar 仍被保留在 classpath 中,导致不同类加载器加载不同版本,Metaspace 中类元数据翻倍。
  • 线程上下文类加载器污染: 某些框架(如 Spring、Hibernate)在初始化时会设置线程上下文类加载器,如果线程池中的线程未被清理,会长期持有旧类加载器引用。
  • 动态代理与反射: 升级后新增的 AOP 切面或动态代理类,每次重新部署都会生成新的代理类,若旧代理类未被卸载,Metaspace 持续增长。
排查建议: 使用 -XX:+TraceClassLoading 和 -XX:+TraceClassUnloading 参数观察类加载与卸载日志。重点关注 [Unloading class ...] 日志行是否持续出现,若长时间无卸载日志,说明类卸载条件被阻塞。