一、什么是类加载机制
Java 程序里的类,只有在 JVM 把它加载到内存后,才能被使用。
所谓类加载机制,就是 JVM 把类从字节码文件加载到内存,并转换成可以运行的 Class 对象的过程。
一个类从加载到可用,通常会经历这几个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
有时还会补充:
- 使用(Using)
- 卸载(Unloading)
二、类加载的生命周期
1. 加载(Loading)
这一阶段主要做三件事:
- 通过类的全限定名获取定义这个类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
Class对象,作为方法区这个类各种数据的访问入口
通俗讲:
就是把 .class 文件内容读进来,并在 JVM 里生成这个类的“身份证”。
2. 验证(Verification)
验证字节码是否符合 JVM 规范,防止非法字节码破坏虚拟机。
常见验证包括:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
作用就是保证:
这个类是安全的、合法的、能被 JVM 正常执行。
3. 准备(Preparation)
给类变量,也就是 static 变量 分配内存,并设置默认初始值。
例如:
public static int a = 10;
在准备阶段,a 先被赋值为默认值 0,不是 10。
注意:
- 这里只处理 类变量
- 实例变量不会在这一步处理,实例变量跟对象一起分配在堆中
4. 解析(Resolution)
把常量池中的符号引用替换成直接引用。
比如:
- 类名
- 方法名
- 字段名
原来只是字符串形式的“符号”,这一步会变成 JVM 能直接定位到的内存地址或偏移量。
5. 初始化(Initialization)
这一步才真正执行类中写的初始化逻辑。
主要就是执行类构造器 <clinit>() 方法。
<clinit>() 是由编译器自动收集下面这些内容组合生成的:
- 静态变量的显式赋值
- 静态代码块
例如:
public class Test {
static int a = 10;
static {
System.out.println("init");
}
}
在初始化阶段才会真正执行:
a = 10- 静态代码块
三、类什么时候会初始化
并不是类一加载就立刻初始化。
只有在首次主动使用类时,才会触发初始化。
常见主动使用场景:
new一个对象- 访问类的静态变量(非 final 常量)
- 调用类的静态方法
- 反射调用
- 初始化子类时,父类先初始化
- JVM 启动时加载主类
例如:
Class.forName("com.test.User");
这通常也会触发类初始化。
四、什么是双亲委派模型
双亲委派模型,本质上是 类加载器的加载规则。
当一个类加载器收到类加载请求时,它不会先自己加载,而是先把请求委托给父加载器 去完成。
只有当父加载器无法完成时,子加载器才会自己尝试加载。
这就是“双亲委派”。
五、Java 中常见类加载器
1. 启动类加载器(Bootstrap ClassLoader)
-
最顶层加载器
-
负责加载
JAVA_HOME/lib核心类库 -
比如
rt.jar(旧版本)、核心基础类等 -
例如:
java.lang.Stringjava.util.*
它不是 Java 对象实现的,通常由 C/C++ 实现。
2. 扩展类加载器(Extension ClassLoader)
JDK 9 之前常这样叫,后面是平台类加载器的概念更常见。
- 负责加载扩展类库
- 比如
jre/lib/ext下的类库(旧版本理解)
3. 应用程序类加载器(Application ClassLoader)
也叫系统类加载器。
- 负责加载应用程序 classpath 下的类
- 我们自己写的大部分类默认都是它加载的
4. 自定义类加载器
开发中也可以自己继承 ClassLoader 实现自定义加载逻辑,比如:
- 热部署
- 模块隔离
- OSGi
- Tomcat 多应用隔离
- 加密类加载
六、双亲委派的工作过程
比如现在应用类加载器要加载 java.lang.String:
第一步
应用类加载器先不自己加载,而是把请求交给父加载器。
第二步
父加载器继续向上委托,最终交给启动类加载器。
第三步
启动类加载器发现 java.lang.String 是核心类,能够加载,于是就完成加载。
第四步
如果父加载器找不到这个类,才会逐层往下,由子加载器自己尝试加载。
七、双亲委派的优点
1. 避免类重复加载
如果每个加载器都自己加载,可能同一个类被加载多次。
双亲委派能尽量保证:
一个类优先由最上层合适的加载器统一加载。
2. 保证 Java 核心类库安全
比如 java.lang.String 这种类,只能由启动类加载器优先加载。
这样就防止有人自己写一个假的 java.lang.String 放到 classpath 里,篡改核心类。
这是双亲委派非常重要的意义。
八、双亲委派是不是父子继承关系
不是。
这里的“父子”是组合关系/委托关系,不是 Java 类继承关系。
也就是说:
- 类加载器之间不是面向对象里的父类子类概念
- 而是“收到请求先往上委托”的层级关系
九、为什么会有“破坏双亲委派”
有些场景下,必须打破双亲委派,否则做不到隔离或灵活加载。
典型场景:
1. Tomcat
Tomcat 里不同 Web 应用可能依赖同名不同版本的类。
如果严格双亲委派,所有应用都走同一个上层加载器,就无法做到应用隔离。
所以 Tomcat 会使用更复杂的类加载机制。
2. SPI 机制
比如 JDBC 驱动。
接口在 rt.jar 或核心类库中,由启动类加载器加载;
但具体实现类在应用的 classpath 中,由应用类加载器加载。
这时就需要通过 线程上下文类加载器 来反向加载,从而“突破”双亲委派。
十、“类加载机制”
Java 的类加载机制是指 JVM 把类的字节码文件加载到内存,并转换为可使用的 Class 对象的过程。类加载主要经历加载、验证、准备、解析、初始化几个阶段。其中加载是读取类字节码并生成 Class 对象,验证是保证字节码合法安全,准备阶段为静态变量分配内存并赋默认值,解析阶段把符号引用转成直接引用,初始化阶段执行静态变量赋值和静态代码块,也就是执行 <clinit> 方法。类通常在首次主动使用时才会被初始化。
十一、“双亲委派模型”
双亲委派模型是 Java 类加载器的一种工作机制。当一个类加载器收到类加载请求时,它不会先自己尝试加载,而是先把请求委托给父加载器;只有父加载器无法完成加载时,子加载器才会自己加载。这样做的好处是避免类被重复加载,同时保证 Java 核心类库的安全性,防止核心类被应用程序篡改。
十二、背诵版
Java 类加载机制是指 JVM 将类的字节码加载到内存并转化为 Class 对象的过程,主要包括加载、验证、准备、解析和初始化五个阶段。加载阶段负责读取字节码并生成 Class 对象,准备阶段为静态变量分配内存并赋默认值,初始化阶段执行静态变量显式赋值和静态代码块。双亲委派模型是类加载器的加载规则,即类加载请求先委托给父加载器处理,只有父加载器无法加载时,子加载器才自己加载。这样可以避免重复加载,并保证核心类库的安全。
十三、面试高频追问
1. static final 常量会触发类初始化吗?
如果是编译期常量,一般不会触发,因为值已经放到调用方常量池里了。
2. Class.forName() 和 ClassLoader.loadClass() 区别
Class.forName():通常会触发初始化loadClass():一般只加载,不一定初始化
3. 为什么自定义的 java.lang.String 不会生效
因为双亲委派下,启动类加载器会优先加载核心类库里的 String
4. 两个类相等的条件是什么
不仅类的全限定名要相同,还必须由同一个类加载器加载。
否则 JVM 会认为它们是两个不同的类。