.class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被 JVM 直接使用的 Java 类型的过程。
类的生命周期
加载(Loading)
加载是类加载的第一个阶段。加载过程
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象
类的来源
- 本地文件系统的
.class文件 - JAR、WAR 包中的类
- 网络下载的类
- 运行时动态生成的类(动态代理)
- 其他文件生成(JSP 编译后的类)
验证(Verification)
确保 Class 文件的字节流符合 JVM 规范,不会危害虚拟机。验证阶段
| 阶段 | 说明 |
|---|---|
| 文件格式验证 | 魔数、版本号、常量池等 |
| 元数据验证 | 语义分析,如是否有父类 |
| 字节码验证 | 数据流和控制流分析 |
| 符号引用验证 | 符号引用能否找到对应的类、方法、字段 |
魔数验证
准备(Preparation)
为类变量(static 变量)分配内存并设置初始值。初始值规则
各类型初始值
| 数据类型 | 初始值 |
|---|---|
| int | 0 |
| long | 0L |
| short | (short) 0 |
| char | ’\u0000’ |
| byte | (byte) 0 |
| boolean | false |
| float | 0.0f |
| double | 0.0d |
| reference | null |
解析(Resolution)
将常量池中的符号引用替换为直接引用。符号引用 vs 直接引用
| 类型 | 说明 |
|---|---|
| 符号引用 | 用一组符号描述目标,如类名、方法名 |
| 直接引用 | 直接指向目标的指针、偏移量或句柄 |
解析类型
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化(Initialization)
执行类构造器<clinit>() 方法的过程。
<clinit>() 方法
初始化时机
以下情况会触发类的初始化:- new 实例化对象
- 访问类的静态变量(非 final 常量)
- 调用类的静态方法
- 反射调用(如
Class.forName()) - 初始化子类时,父类先初始化
- main 方法所在的类
不会触发初始化的情况
类加载器
类加载器类型
获取类加载器
双亲委派模型
工作原理
源码分析
双亲委派的优点
- 避免类的重复加载
- 保证核心类的安全:防止用户自定义类覆盖核心类
自定义类加载器
实现方式
继承ClassLoader 并重写 findClass 方法。
使用示例
打破双亲委派
常见场景
- JNDI、JDBC:核心类需要加载 SPI 实现类
- 热部署:如 Tomcat 的 WebAppClassLoader
- 模块化:OSGi
线程上下文类加载器
Tomcat 类加载器
- 每个 Web 应用有独立的类加载器
- 优先加载 Web 应用自己的类
- 实现应用隔离
小结
- 类加载过程:加载 → 验证 → 准备 → 解析 → 初始化
- 准备阶段:静态变量赋初始值(零值),final 常量直接赋值
- 初始化阶段:执行
<clinit>()方法 - 双亲委派:优先委托父加载器加载,保证类加载的唯一性和安全性
- 打破双亲委派:热部署、SPI、模块化等场景