02 - 类加载机制
定位: JVM如何将Class文件从静态字节流转化为运行时类对象,并管理其生命周期 面试高频度: ⭐⭐⭐⭐⭐
一、类加载在 JVM 体系中的位置
类加载在 JVM 启动→运行全流程中扮演桥梁角色:它接收 Class 文件的静态结构,将其转化为方法区的运行时数据结构,并在堆上生成 Class<?> 对象供反射使用。没有类加载,字节码只是一堆字节流。
- 向上: 消费 Class 文件格式,解析常量池、字段表、方法表
- 向下: 在方法区(元空间)构建类元数据,在堆上创建 Class 对象
- 横向: 通过双亲委派模型维护 Java 类型体系的安全性
二、类加载生命周期时间线
JVM 启动 → 加载第一个类 (Bootstrap ClassLoader 加载核心类)
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 类加载生命周期 (7 阶段交错执行) │
│ │
│ ༌༌༌༌༌༌ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ │
│ ┌─────┐ │
│ │ 加载 │═══════╗ │
│ └─────┘ ║ │
│ ┌───────┐ ║ │
│ │ 验证 │═════╬═══════════════════════════════════╗ │
│ └───────┘ ║ ║ │
│ ┌───────┐ ║ ║ │
│ │ 准备 │═════╝═══════════════════════════════════╝ │
│ └───────┘ │
│ ┌───────┐ │
│ │ 解析 │═══════╗ ← 可延迟到第一次使用 (迟到绑定) │
│ └───────┘ ║ │
│ ┌─────────┐ ║ │
│ │ 初始化 │═════╝══════ 触发时机: new / getstatic / 反射等 │
│ └─────────┘ │
│ ┌───────┐ │
│ │ 使用 │══════════════════════════ │
│ └───────┘ ║ │
│ ┌───────┐ ║ │
│ │ 卸载 │════════════════════════╝ │
│ └───────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
垃圾回收可卸载 → 如果 ClassLoader 不可达关键认知: 7 阶段不是严格的串行流水线。加载和验证可以并行(边读字节流边校验);准备分配内存但不赋初值;解析可以一直推迟到字节码指令第一次引用符号引用为止(迟到绑定);初始化只触发一次(JVM 保证线程安全)。
三、子专题导航
| # | 主题 | 面试频率 | 核心内容 |
|---|---|---|---|
| **类加载生命周期** | 类加载 7 阶段详解 | ⭐⭐⭐⭐⭐ | 加载→验证→准备→解析→初始化→使用→卸载,阶段交错动机,主动引用 vs 被动引用 |
| **双亲委派模型与打破** | 双亲委派模型与打破 | ⭐⭐⭐⭐⭐ | 三层 ClassLoader 层级、向上委派的安全动机、SPI 打破委派、TCCL 桥接 |
| **自定义类加载器实战** | 自定义类加载器实战 | ⭐⭐⭐⭐ | findClass vs loadClass 职责分离、defineClass 原理、加密类加载、热部署 |
| **Tomcat与Spring类加载分析** | Tomcat 与 Spring 类加载分析 | ⭐⭐⭐⭐ | Tomcat 多级 ClassLoader、Webapp 反向委派、Spring Boot fat jar 嵌套加载 |
| **OSGI与SPI模块化** | OSGI 与 SPI 模块化 | ⭐⭐⭐ | OSGI Bundle 网状依赖、SPI ServiceLoader 原理、JDBC 驱动加载全链路、JPMS |
四、核心考点速记
1. 双亲委派模型
ClassLoader.loadClass(name)
│
▼
┌─ 检查类是否已加载 ──→ 已加载 → 直接返回
│
▼ (未加载)
┌─ 委托父 ClassLoader ──→ 父能加载 → 父返回
│ 父不能加载 → 自己尝试
▼
┌─ findClass(name) → defineClass(byte[]) → 返回 Class安全动机: java.lang.String 只能由 Bootstrap ClassLoader 加载。即使你写一个同名的 java.lang.String,双亲委派也会让 Bootstrap 的 String 优先——恶意代码根本无法替换核心类。
打破动机: SPI (JDBC、JNDI 等)。DriverManager 在 rt.jar(Bootstrap 加载),但 JDBC 驱动是应用 jar(AppClassLoader 加载)。Bootstrap 加载的代码需要调用应用类——委派方向反了。解决方案:线程上下文类加载器 (TCCL)。
2. SPI 打破委派的全链路
DriverManager.getConnection(url)
│ DriverManager 由 Bootstrap ClassLoader 加载 (rt.jar)
▼
ServiceLoader.load(Driver.class)
│ 用 TCCL (AppClassLoader) 加载驱动实现
▼
读取 META-INF/services/java.sql.Driver
│ (来自 mysql-connector-java.jar)
▼
com.mysql.cj.jdbc.Driver
│ TCCL 加载 → 触发 Driver 的静态块 → DriverManager.registerDriver()
▼
返回可用 Driver 实现3. Tomcat 类加载隔离
Bootstrap (JRE核心)
↓
System (Tomcat启动)
↓
Common (共享库: Servlet API, Tomcat自身类)
├── Catalina (Tomcat内部)
└── Shared (所有Web应用共享)
├── Webapp1 (应用A: 先自己加载, 再委派)
└── Webapp2 (应用B: 同名类不冲突)为什么 WebappClassLoader 打破双亲委派: Web 应用需要能覆盖共享库中的同名校本。例如应用A用 Spring 4,应用B用 Spring 5——如果先委派给 Shared,两个应用永远用同一个版本。WebappClassLoader 先自己尝试加载,找不到再委派向上。
五、面试高频追问一览
| 追问 | 关联笔记 | 频次 |
|---|---|---|
| 类加载有哪几个阶段?哪些可以并行? | **类加载生命周期** | 极高 |
| 主动引用和被动引用的区别? | **类加载生命周期** | 极高 |
| 双亲委派模型是什么?为什么要这样设计? | **双亲委派模型与打破** | 极高 |
| SPI 是如何打破双亲委派的? | **OSGI与SPI模块化** | 高 |
| 如何自定义一个 ClassLoader? | **自定义类加载器实战** | 高 |
| Tomcat 的类加载机制? | **Tomcat与Spring类加载分析** | 中 |
为什么 static final 常量不需要触发类初始化? | **类加载生命周期** | 高 |
ClassNotFoundException 和 NoClassDefFoundError 的区别? | **自定义类加载器实战** | 中 |
| Java 9 的模块化对类加载有什么影响? | **OSGI与SPI模块化** | 中低 |
📚 相关文件
- **Class文件结构** — Class 文件是类加载的输入
- **方法区与元空间** — 类加载在方法区构建类元数据
- **运行时常量池** — 符号引用→直接引用在运行时常量池中完成
- **JVM文库设计文档**