1.2.5 Java 性能与调优 · 类加载机制与 Tomcat 打破双亲委派
类加载机制 + 打破双亲委派的 3 个真实场景。
1. 类加载 5 阶段
JVM 把 class 从字节码加载到内存并完成可用化,严格按 加载 → 验证 → 准备 → 解析 → 初始化 五阶段顺序推进,中间阶段允许交叉(主要是解析可在初始化之后),整体由 JLS §12.4 规定。
| 阶段 | 输入 | 主要动作 | 输出 |
|---|---|---|---|
| Loading | .class 字节流 |
通过类名读字节流、生成 Class 对象 | 方法区 Class 实例 + 堆 Class 对象 |
| Verification | Class 文件结构 | 文件格式 / 元数据 / 字节码 / 符号引用校验 | 通过校验的安全 Class |
| Preparation | 类变量表 | 为 static 变量分配内存并赋零值 | 零值化的类变量 |
| Resolution | 常量池符号引用 | 把符号引用替换为直接引用 | 已解析的运行时引用 |
| Initialization | 类变量 + 静态块 | 执行 <clinit>():static 赋值 + static {} |
类完成初始化,可被使用 |
初始化触发时机(JLS §12.4.1): new / getstatic / putstatic / invokestatic 这 4 条字节码指令,反射调用,子类初始化触发父类初始化,主类 main。被动引用(如子类引用父类 static 字段)不会触发子类 <clinit>,这是面试常考点。<clinit>() 由编译器自动收集类变量赋值与 static {} 合并生成,线程安全由 JVM 保证,但写法上要避免循环依赖。
2. 双亲委派模型(Parent Delegation Model)
JVM 自带 3 层 ClassLoader,加载请求自底向上委派、类查找自顶向下执行,目的是保证核心类由启动类加载器统一加载,防止被同名类覆盖。
| 加载器 | 实现类(JDK 8) | 负责加载路径 | 父加载器 |
|---|---|---|---|
| Bootstrap ClassLoader | C++ 实现,getClassLoader() 返回 null |
$JAVA_HOME/lib/rt.jar、resources.jar 等核心类 |
无 |
| Extension ClassLoader | sun.misc.Launcher$ExtClassLoader |
$JAVA_HOME/lib/ext/*.jar |
Bootstrap |
| Application ClassLoader | sun.misc.Launcher$AppClassLoader |
-classpath / -cp 指定的类与 jar |
Extension |
| 自定义 ClassLoader | 继承 ClassLoader |
用户自定义路径 | 通常为 AppClassLoader |
委派逻辑在 ClassLoader.loadClass():
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try { c = parent.loadClass(name, false); }
catch (ClassNotFoundException) { /* parent 找不到 */ }
if (c == null) c = findClass(name); // 自己找
}
if (resolve) resolveClass(c);
return c;
}
}
父类无法加载时才由自己 findClass() 兜底——这就是”双亲委派”。但严格意义上它不是强制约束,而是一种推荐实现,任何自定义加载器都可以打破(下文展开)。注意 JDK 7+ 加入了并行类加载(getClassLoadingLock 用 ConcurrentHashMap),避免加载死锁。
3. 打破双亲委派的 3 个场景
Java 生态中至少有 3 个主流场景必须反向委派:基础类型由 Bootstrap 加载,但 SPI 实现放在 ClassPath 下,加载器必须反向”问儿子”。
| 场景 | 问题 | 解决方案 |
|---|---|---|
| JDBC SPI | DriverManager 在 rt.jar,由 Bootstrap 加载,但 mysql-connector-java 的 Driver 实现在 ClassPath | DriverManager 用 Thread.currentThread().getContextClassLoader() 拿到 AppClassLoader 加载 Driver |
| Tomcat WebAppClassLoader | 多个 WebApp 各自依赖 spring/日志版本,不能让 AppClassLoader 统一加载 | 自定义 WebappClassLoader,先自己 WEB-INF/lib 找,再委派 parent |
| OSGi | 同一模块多版本共存(Equinox / Felix),需要按模块动态切换 | Bundle ClassLoader 采用网状委派图,每个 bundle 互相 import/export |
关键 API: Thread.setContextClassLoader() 是 SPI 机制的核心——Bootstrap 类需要”借用”应用加载器加载实现类,而自己又没有 AppClassLoader 引用,所以把加载器存在线程上下文里。JDBC 4.0 之后还引入了 ServiceLoader 机制,通过 META-INF/services/ 文件按 SPI 名自动发现实现类,本质也是同一套反向加载思路。
调研依据:Tomcat 9.x 源码
org.apache.catalina.loader.WebappClassLoaderBase#loadClass显式注释了”Cache the class loader lock,防止递归 loadClass 死锁”,并在委派前优先findClass()自己目录。
4. Tomcat 类加载实战:为什么 WebApp 之间能类隔离
Tomcat 同时部署 AppA(Spring 5) 和 AppB(Spring 6),两者 spring-core jar 互不污染,关键是 WebappClassLoaderBase 重写了 loadClass() 默认顺序。
| 顺序 | 检查位置 | 目的 |
|---|---|---|
| 1 | JVM 缓存 (findLoadedClass) |
避免重复加载 |
| 2 | 本 WebApp 的 /WEB-INF/classes 与 /WEB-INF/lib |
类隔离核心:各 App 自己的类优先 |
| 3 | System class (Bootstrap / Ext / AppClassLoader) | 共享 JRE 与 Tomcat 公共类,如 servlet-api |
| 4 | Common ClassLoader (shared/lib) |
Tomcat 共享 jar,如 el-api |
| 5 | 父 AppClassLoader 兜底 | 加载第三方库最后兜底 |
典型故障排查: 当 App 报 ClassNotFoundException: org.apache.xerces...,先把对应 jar 放到 WEB-INF/lib/ 而非 tomcat/lib/,避免污染其他 App。Tomcat 6 之前默认先委托父加载器,导致 App 间互相覆盖,Tomcat 6+ 改为先自己再父才解决。
反之,如果你希望 jar 被所有 App 共享(比如统一日志组件),放到 $CATALINA_HOME/lib/ 即可——它由 Common ClassLoader 加载,所有 WebApp 都能看到。另外 Tomcat 提供 <Loader delegate="true"/> 配置项,在 context.xml 里可强制回到”传统双亲委派”,用于兼容老应用,但会牺牲隔离性。
5. Java 9+ 模块化系统(JPMS)对类加载的影响
Java 9 引入 Jigsaw / JPMS,把”jar 路径即模块”换成显式 module 声明,类加载器结构也由”线性三层”变为”模块化平台”。
| 维度 | Java 8 | Java 9+ |
|---|---|---|
| 加载路径 | rt.jar / tools.jar 一个 fat jar | jrt-fs 虚拟文件系统,按 java.base / java.sql 等模块切片 |
| 类可见性 | public = 全局可见 | 模块未导出(exports)的包外部不可访问 |
| ClassLoader | 三层(bootstrap/ext/app)+ 自定义 | 平台类加载器(PlatformClassLoader) 替代 ExtClassLoader,负责 java.sql / java.xml 等非核心模块 |
| 反射限制 | 无 | 强封装:未 open 的模块默认不允许 setAccessible(true) |
核心类加载变化: ClassLoader.getSystemClassLoader() 在 JDK 9+ 不再直接返回 AppClassLoader,而是返回 PlatformClassLoader 的子类 AppClassLoader;Bootstrap 加载 java.base / java.lang,Platform 加载 java.sql / java.logging,Application 加载 classpath 上的所有 named module / unnamed module。模块路径(--module-path)与 classpath 共存,前者走模块图,后者走 unnamed module 平铺。
实战影响: 框架如 Spring Boot 3 强制要求 Java 17+,运行在 JPMS 之上,遇到 IllegalAccessError 多半是模块未导出或未 open——加 --add-opens java.base/java.lang=ALL-UNNAMED 即可。这是模块化最常见的排错命令,记住。JPMS 至今在业务项目落地有限,主要是库作者要解决”如何在模块化平台下暴露 SPI”,而大多数应用仍跑在 classpath(unnamed module)上以避免复杂声明。