文章目录
- JVM虚拟机类加载机制:系统性知识体系总结
-
- 一、类加载机制概述
-
- [1.1 核心定义](#1.1 核心定义)
- [1.2 类的完整生命周期](#1.2 类的完整生命周期)
- [1.3 类加载子系统的组成](#1.3 类加载子系统的组成)
- 二、类加载的时机
-
- [2.1 主动引用(必触发初始化)](#2.1 主动引用(必触发初始化))
- [2.2 被动引用(不触发初始化)](#2.2 被动引用(不触发初始化))
- [2.3 重要区别](#2.3 重要区别)
- 三、类加载核心五阶段详解
-
- [3.1 加载阶段(Loading)](#3.1 加载阶段(Loading))
- [3.2 验证阶段(Verification)](#3.2 验证阶段(Verification))
- [3.3 准备阶段(Preparation)](#3.3 准备阶段(Preparation))
- [3.4 解析阶段(Resolution)](#3.4 解析阶段(Resolution))
- [3.5 初始化阶段(Initialization)](#3.5 初始化阶段(Initialization))
- 四、类加载器体系
-
- [4.1 类加载器的层次结构](#4.1 类加载器的层次结构)
- [4.2 双亲委派模型](#4.2 双亲委派模型)
- [4.3 打破双亲委派模型](#4.3 打破双亲委派模型)
- 五、类的卸载
-
- [5.1 类卸载的条件](#5.1 类卸载的条件)
- [5.2 类卸载的过程](#5.2 类卸载的过程)
- 六、常见面试考点与易错点
-
- [6.1 高频面试题](#6.1 高频面试题)
- [6.2 常见易错点](#6.2 常见易错点)
- 七、总结
- JVM类加载机制:面试问答卡片+代码验证示例

JVM虚拟机类加载机制:系统性知识体系总结
一、类加载机制概述
1.1 核心定义
类加载机制是JVM将class文件 中的二进制数据读取到内存中,并对其进行验证、转换、解析和初始化 ,最终形成可以被JVM直接使用的java.lang.Class对象的过程。
1.2 类的完整生命周期
一个类从被加载到JVM内存开始,到卸载出内存为止,其完整生命周期包括7个阶段:
加载(Loading) → 验证(Verification) → 准备(Preparation) → 解析(Resolution) → 初始化(Initialization) → 使用(Using) → 卸载(Unloading)
关键要点:
- 前5个阶段构成类加载的核心流程
- 验证、准备、解析三个阶段统称为链接(Linking)阶段
- 各阶段按顺序开始,但不一定按顺序完成(解析可能在初始化之后进行)
- 除加载阶段外,其余阶段完全由JVM主导控制
1.3 类加载子系统的组成
- 类加载器(ClassLoader):负责加载类的二进制字节流
- 字节码验证器:确保class文件的合法性和安全性
- 运行时常量池:存储类中的常量和符号引用
- 方法区/元空间:存储类的元数据信息
二、类加载的时机
2.1 主动引用(必触发初始化)
《Java虚拟机规范》严格规定,有且仅有以下5种情况会立即触发类的初始化(如果类尚未加载、验证、准备,则会先完成这些阶段):
| 主动引用场景 | 字节码指令 | 示例代码 |
|---|---|---|
| 创建类的实例 | new |
User user = new User(); |
| 访问/修改类的静态变量(非final) | getstatic/putstatic |
int age = User.age; |
| 调用类的静态方法 | invokestatic |
MathUtil.add(1, 2); |
| 反射调用类 | - | Class.forName("com.example.User"); |
| 初始化子类时父类未初始化 | - | class Child extends Parent {} |
| 虚拟机启动时的主类 | - | public static void main(String[] args) |
| JDK1.7+动态语言支持 | - | MethodHandle解析结果为静态方法句柄 |
2.2 被动引用(不触发初始化)
除上述主动引用外,所有其他引用方式都不会触发类的初始化,称为被动引用:
-
通过子类引用父类的静态变量:只会初始化父类,不会初始化子类
java// 只会输出"Parent initialized!" System.out.println(Child.parentStaticVar); -
通过数组定义类的引用:不会触发元素类的初始化
java// 不会输出"User initialized!" User[] users = new User[10]; -
引用类的final常量:常量在编译阶段就存入调用类的常量池中
java// 不会输出"Constants initialized!" System.out.println(Constants.PI);
2.3 重要区别
- 类加载 ≠ 类初始化:类加载包括加载、验证、准备、解析、初始化五个阶段
- Class.forName() vs ClassLoader.loadClass() :
Class.forName():会触发类的初始化ClassLoader.loadClass():只完成加载和链接阶段,不触发初始化
三、类加载核心五阶段详解
3.1 加载阶段(Loading)
核心目标:查找并加载类的二进制字节流,生成Class对象
JVM完成的三件事:
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆内存 中生成一个代表这个类的
java.lang.Class对象,作为方法区类数据的访问入口
二进制字节流的来源:
- 本地文件系统(最常见:.class文件)
- 压缩包(JAR、WAR、EAR)
- 网络(Applet、RMI)
- 运行时动态生成(ASM、CGLIB、Lambda表达式)
- 数据库(少见)
- 其他文件(如JSP编译生成的class文件)
特点:
- 这是类加载过程中唯一可以由用户自定义类加载器参与的阶段
- 加载阶段与链接阶段的部分内容(如部分字节码验证)可能交叉进行
3.2 验证阶段(Verification)
核心目标:确保被加载的类的正确性,防止恶意代码危害JVM安全
验证的四个主要步骤:
-
文件格式验证
- 验证字节流是否符合Class文件格式规范
- 检查魔数(0xCAFEBABE)、版本号、常量池类型等
- 这是唯一直接操作字节流的验证阶段
-
元数据验证
- 对类的元数据信息进行语义校验
- 检查类的继承关系、方法和字段的合法性
- 确保没有违反Java语言规范的情况
-
字节码验证
- 对类的方法体进行数据流和控制流分析
- 确保字节码指令不会做出危害JVM安全的行为
- 这是最复杂的验证阶段
-
符号引用验证
- 验证符号引用的合法性
- 确保符号引用能被正确解析为直接引用
- 发生在解析阶段之前
注意 :验证阶段不是必须的,如果代码已经被反复验证过,可以通过-Xverify:none参数关闭大部分类验证,以缩短类加载时间。
3.3 准备阶段(Preparation)
核心目标 :为类变量(static变量)分配内存并设置默认初始值
关键要点:
- 内存分配在方法区(JDK8+为元空间)
- 只分配类变量的内存,不分配实例变量的内存
- 设置的是默认初始值,而不是程序员定义的初始值
- 默认初始值表:
| 数据类型 | 默认初始值 |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| char | '\u0000' |
| boolean | false |
| 引用类型 | null |
特殊情况:
-
如果类变量被
final修饰(即常量),则在准备阶段会被赋值为程序员定义的值java// 准备阶段:value = 123(而不是0) public static final int value = 123;
3.4 解析阶段(Resolution)
核心目标 :将符号引用 转换为直接引用
基本概念:
- 符号引用:以一组符号来描述所引用的目标,与JVM的内存布局无关
- 直接引用:可以直接指向目标的内存地址,与JVM的内存布局相关
解析的主要类型:
- 类或接口的解析:验证引用的类或接口是否已加载
- 字段解析:确定字段在类中的实际内存位置
- 方法解析:确定方法的直接调用地址(如虚方法表索引)
- 接口方法解析:处理接口方法的多实现问题
解析时机:
- 静态解析:在类加载阶段完成(如final方法、私有方法、静态方法)
- 动态解析:在运行时完成(如虚方法调用,依赖运行时类型确定目标方法)
- 解析阶段不一定在初始化之前完成,也可能在初始化之后进行
3.5 初始化阶段(Initialization)
核心目标 :执行类构造器<clinit>()方法,对类变量进行程序员定义的初始化
()方法的特点:
- 由编译器自动收集 类中所有静态变量的赋值语句 和静态代码块合并而成
- 静态代码块只能访问定义在它之前的静态变量,定义在它之后的静态变量只能赋值,不能访问
- JVM会保证在子类的
<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕 - JVM会保证
<clinit>()方法在多线程环境下被正确加锁和同步,确保一个类只被初始化一次 - 如果一个类没有静态变量和静态代码块,编译器不会为它生成
<clinit>()方法
初始化顺序:
父类静态变量 → 父类静态代码块 → 子类静态变量 → 子类静态代码块 →
父类实例变量 → 父类实例代码块 → 父类构造方法 →
子类实例变量 → 子类实例代码块 → 子类构造方法
重要说明:
- 这是类加载过程的最后一步
- 到了初始化阶段,才真正开始执行类中定义的Java代码
- 接口的初始化不需要先初始化其父接口,只有在使用父接口的常量时才会初始化父接口
四、类加载器体系
4.1 类加载器的层次结构
JVM提供了三种类加载器,形成了双亲委派模型的层次结构:
| 类加载器 | 负责加载的类 | 加载路径 |
|---|---|---|
| 启动类加载器(Bootstrap ClassLoader) | Java核心类库 | JAVA_HOME/jre/lib |
| 扩展类加载器(Extension ClassLoader) | Java扩展类库 | JAVA_HOME/jre/lib/ext |
| 应用程序类加载器(Application ClassLoader) | 应用程序类 | 应用程序classpath |
| 自定义类加载器(Custom ClassLoader) | 用户自定义类 | 用户指定路径 |
注意:启动类加载器是用C++实现的,不是Java类,因此无法在Java代码中直接获取它的引用。
4.2 双亲委派模型
核心思想 :当一个类加载器收到类加载请求时,它首先不会自己去加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器无法完成这个加载请求时,子加载器才会尝试自己去加载。
工作流程:
- 检查类是否已经被当前类加载器加载过
- 如果没有加载过,调用父类加载器的
loadClass()方法 - 如果父类加载器为null,则使用启动类加载器
- 如果父类加载器加载失败,调用自己的
findClass()方法尝试加载
优点:
- 安全性:防止核心类库被恶意篡改
- 避免重复加载:同一个类只会被加载一次
- 类层次划分:不同层次的类由不同的类加载器加载,实现了类的隔离
4.3 打破双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是JVM推荐的类加载器实现方式。在实际应用中,有很多场景需要打破双亲委派模型:
打破双亲委派的三大经典场景:
-
SPI服务发现机制(JDBC、JNDI等)
- 问题:核心接口由启动类加载器加载,而实现类由应用类加载器加载
- 解决方案:使用线程上下文类加载器,让父类加载器可以访问子类加载器加载的类
-
Web容器(Tomcat、Jetty等)
- 问题:需要隔离不同Web应用的类,避免类冲突
- 解决方案:每个Web应用使用独立的类加载器,优先加载本地类,而不是委派给父类加载器
-
热部署(JRebel、Spring Boot DevTools等)
- 问题:需要在不重启JVM的情况下重新加载修改后的类
- 解决方案:使用自定义类加载器,当类发生变化时,销毁旧的类加载器,创建新的类加载器重新加载类
打破双亲委派的实现方式:
- 重写
ClassLoader的loadClass()方法,修改委派逻辑 - 使用线程上下文类加载器
- 使用OSGi等模块化框架
五、类的卸载
5.1 类卸载的条件
类的卸载需要同时满足以下三个严格的条件:
- 该类的所有实例对象都已经被回收
- 该类的
java.lang.Class对象没有任何引用 - 加载该类的类加载器已经被回收
5.2 类卸载的过程
当满足上述条件时,JVM会在垃圾回收时对类进行卸载,释放该类在方法区/元空间中占用的内存。
注意:
- 由启动类加载器加载的核心类库永远不会被卸载
- 由应用程序类加载器加载的类在正常情况下也很难被卸载
- 只有由自定义类加载器加载的类才有可能被卸载
六、常见面试考点与易错点
6.1 高频面试题
- 简述JVM类加载的过程?
- 什么是双亲委派模型?它有什么优点?
- 有哪些情况会触发类的初始化?
- 准备阶段和初始化阶段的区别是什么?
- 符号引用和直接引用的区别是什么?
- 为什么要打破双亲委派模型?有哪些场景?
Class.forName()和ClassLoader.loadClass()的区别是什么?- 类的卸载需要满足哪些条件?
6.2 常见易错点
- 混淆类加载和类初始化:类加载包括五个阶段,初始化只是其中之一
- 准备阶段赋值错误:准备阶段只给类变量赋默认值,final常量除外
- 静态代码块执行顺序错误:静态代码块只能访问定义在它之前的静态变量
- 认为所有引用都会触发初始化:只有主动引用才会触发初始化
- 认为双亲委派模型不能被打破:实际上在很多框架中都打破了双亲委派模型
七、总结
JVM类加载机制是Java语言的核心特性之一,它实现了类的动态加载和隔离,为Java的跨平台性、安全性和灵活性提供了基础。理解类加载机制不仅有助于我们编写更高效、更安全的Java代码,也是深入理解各种Java框架(如Spring、Tomcat、MyBatis等)工作原理的关键。
类加载的五个阶段(加载→验证→准备→解析→初始化)各有其明确的职责和执行顺序,而双亲委派模型则是类加载器体系的核心设计思想。在实际开发中,我们不仅要遵循JVM的规范,还要学会灵活运用类加载机制来解决各种复杂的问题。
JVM类加载机制:面试问答卡片+代码验证示例
第一部分:可直接背诵的面试问答卡片
一、基础概念类
-
Q:什么是JVM类加载机制?
A:JVM将class文件中的二进制数据读取到内存,经过验证、转换、解析和初始化,最终形成可直接使用的java.lang.Class对象的过程。
-
Q:类的完整生命周期包括哪7个阶段?
A:加载→验证→准备→解析→初始化→使用→卸载。前5个阶段构成类加载核心流程,验证、准备、解析统称为链接阶段。
-
Q:类加载各阶段是严格按顺序完成的吗?
A:各阶段按顺序开始,但不一定按顺序完成。解析阶段可能在初始化之后进行(动态绑定)。
二、类加载时机类
-
Q:有且仅有哪几种情况会触发类的初始化(主动引用)?
A:①创建类的实例(new);②访问/修改非final静态变量;③调用静态方法;④反射调用;⑤初始化子类时父类未初始化;⑥虚拟机启动时的主类;⑦JDK1.7+动态语言支持的静态方法句柄。
-
Q:什么是被动引用?举3个例子。
A:不会触发类初始化的引用方式。例子:①通过子类引用父类静态变量;②通过数组定义类的引用;③引用类的final常量。
-
Q:Class.forName()和ClassLoader.loadClass()的核心区别是什么?
A:Class.forName()会触发类的初始化;ClassLoader.loadClass()只完成加载和链接阶段,不触发初始化。
三、五阶段详解类
-
Q:加载阶段JVM完成哪三件事?
A:①通过全限定名获取二进制字节流;②将静态存储结构转化为方法区运行时数据结构;③在堆中生成java.lang.Class对象作为访问入口。
-
Q:验证阶段包括哪四个主要步骤?
A:文件格式验证→元数据验证→字节码验证→符号引用验证。
-
Q:准备阶段的核心工作是什么?
A:为类变量(static变量)在方法区分配内存并设置默认初始值。注意:只处理类变量,不处理实例变量;final常量会直接赋值为程序员定义的值。
-
Q:符号引用和直接引用的区别是什么?
A:符号引用是一组描述目标的符号,与JVM内存布局无关;直接引用是指向目标的内存地址,与JVM内存布局相关。
-
Q:初始化阶段的核心工作是什么?
A:执行类构造器()方法,对类变量进行程序员定义的初始化。
-
Q:()方法有哪些特点?
A:①由静态变量赋值语句和静态代码块合并而成;②父类()先于子类执行;③JVM保证多线程环境下只执行一次;④没有静态成员则不会生成该方法。
四、类加载器体系类
-
Q:JDK8默认的三种类加载器及其负责加载的类是什么?
A:①启动类加载器:加载JAVA_HOME/jre/lib下的核心类库(C++实现);②扩展类加载器:加载JAVA_HOME/jre/lib/ext下的扩展类库;③应用程序类加载器:加载应用程序classpath下的类。
-
Q:什么是双亲委派模型?
A:类加载器收到请求时,先委派给父类加载器加载,所有请求最终传送到启动类加载器。只有父类加载器无法加载时,子加载器才尝试自己加载。
-
Q:双亲委派模型的优点是什么?
A:①安全性:防止核心类库被恶意篡改;②避免重复加载:同一个类只会被加载一次;③类层次划分:实现类的隔离。
-
Q:为什么要打破双亲委派模型?举3个经典场景。
A:当父类加载器需要加载子类加载器中的类时需要打破。场景:①SPI服务发现(JDBC、JNDI);②Web容器(Tomcat)的应用隔离;③热部署(JRebel、Spring Boot DevTools)。
五、类卸载类
-
Q:类卸载需要同时满足哪三个条件?
A:①该类的所有实例对象都已被回收;②该类的Class对象没有任何引用;③加载该类的类加载器已被回收。
-
Q:哪些类永远不会被卸载?
A:由启动类加载器加载的核心类库永远不会被卸载;由应用程序类加载器加载的类在正常情况下也很难被卸载。
第二部分:代码验证示例
示例1:主动引用vs被动引用验证
java
class Parent {
public static int parentStaticVar = 100;
static {
System.out.println("Parent initialized!");
}
}
class Child extends Parent {
static {
System.out.println("Child initialized!");
}
}
class Constants {
public static final int PI = 314;
static {
System.out.println("Constants initialized!");
}
}
public class ClassLoadingTest1 {
public static void main(String[] args) {
// 被动引用1:通过子类引用父类静态变量
System.out.println("=== 被动引用1 ===");
System.out.println(Child.parentStaticVar); // 只输出Parent initialized!
// 被动引用2:通过数组定义类的引用
System.out.println("\n=== 被动引用2 ===");
Child[] children = new Child[10]; // 无输出
// 被动引用3:引用final常量
System.out.println("\n=== 被动引用3 ===");
System.out.println(Constants.PI); // 无输出
// 主动引用:创建子类实例
System.out.println("\n=== 主动引用 ===");
new Child(); // 先输出Parent initialized!,再输出Child initialized!
}
}
预期输出:
=== 被动引用1 ===
Parent initialized!
100
=== 被动引用2 ===
=== 被动引用3 ===
314
=== 主动引用 ===
Child initialized!
示例2:准备阶段vs初始化阶段赋值验证
java
class PrepareTest {
// 准备阶段:a=0;初始化阶段:a=10
public static int a = 10;
// 准备阶段:b=20(final常量直接赋值)
public static final int b = 20;
static {
System.out.println("PrepareTest initialized!");
System.out.println("a=" + a);
System.out.println("b=" + b);
}
}
public class ClassLoadingTest2 {
public static void main(String[] args) {
// 触发初始化
new PrepareTest();
}
}
预期输出:
PrepareTest initialized!
a=10
b=20
示例3:静态代码块执行顺序验证
java
class StaticOrderTest {
static {
// 可以赋值,但不能访问(编译错误)
// System.out.println(x);
x = 20;
}
public static int x = 10;
static {
System.out.println("x=" + x);
}
}
public class ClassLoadingTest3 {
public static void main(String[] args) {
new StaticOrderTest();
}
}
预期输出:
x=10
解释:静态代码块和静态变量按定义顺序执行。先执行第一个静态代码块将x赋值为20,然后执行x=10将x覆盖为10,最后执行第二个静态代码块输出x=10。
示例4:类初始化完整顺序验证
java
class SuperClass {
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类实例代码块");
}
public SuperClass() {
System.out.println("父类构造方法");
}
}
class SubClass extends SuperClass {
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类实例代码块");
}
public SubClass() {
System.out.println("子类构造方法");
}
}
public class ClassLoadingTest4 {
public static void main(String[] args) {
System.out.println("=== 第一次创建子类实例 ===");
new SubClass();
System.out.println("\n=== 第二次创建子类实例 ===");
new SubClass();
}
}
预期输出:
=== 第一次创建子类实例 ===
父类静态代码块
子类静态代码块
父类实例代码块
父类构造方法
子类实例代码块
子类构造方法
=== 第二次创建子类实例 ===
父类实例代码块
父类构造方法
子类实例代码块
子类构造方法
解释:静态成员只在类第一次初始化时执行一次;实例成员每次创建对象时都会执行。
示例5:双亲委派模型验证
java
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取应用程序类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("应用程序类加载器:" + appClassLoader);
// 获取扩展类加载器
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("扩展类加载器:" + extClassLoader);
// 获取启动类加载器(返回null,因为是C++实现)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("启动类加载器:" + bootstrapClassLoader);
// 验证String类由启动类加载器加载
System.out.println("String类的类加载器:" + String.class.getClassLoader());
// 验证自定义类由应用程序类加载器加载
System.out.println("自定义类的类加载器:" + ClassLoaderTest.class.getClassLoader());
}
}
预期输出(JDK8环境):
应用程序类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器:sun.misc.Launcher$ExtClassLoader@1b6d3586
启动类加载器:null
String类的类加载器:null
自定义类的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2