【JVM】Java类加载机制
什么是类加载?
在 Java 的世界里,每一个类或接口在经过编译后,都会生成对应的 .class
字节码文件。
所谓类加载机制 ,就是 JVM 将这些 .class
文件中的二进制数据加载到内存中 ,并对其进行校验、解析、初始化 等一系列操作。最终,每个类都会在方法区(或元空间)中保留一份结构化的类信息(元数据),并在Java 堆中创建一个 java.lang.Class
类型的对象,供程序运行时使用。
从 JVM 的角度看,一个类的生命周期包括以下 7 个阶段:
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
其中,前五个阶段(加载、验证、准备、解析、初始化)统称为类的加载过程 。其中,验证、准备和解析这三个阶段可以统称为连接 。
需要注意的是,类加载的五个阶段并不是严格按顺序线性执行的,而是相互交叉、动态混合的过程。
例如:
- 部分验证操作(如文件格式验证)可能在加载
.class
文件的过程中就被触发。- 符号引用的解析可能被延迟到真正使用时才发生。
类加载的详细过程
1.加载
-
任务: 查找并加载类的二进制数据(通常是
.class
文件,但来源可以多样)。 -
过程:
- 通过类的全限定名 (如
java.lang.String
或com.example.MyClass
)获取定义此类的二进制字节流。这个字节流来源可以是文件系统(最常见的)、网络、ZIP/JAR包、运行时计算生成(动态代理)、数据库等等。
类的全限定名就是类的完整名称,即:包名 + 类名(如
java.lang.String
或com.example.MyClass
)- 将这个字节流所代表的静态存储结构转换为方法区 的运行时数据结构。
- 在堆(Heap) 内存中创建一个代表这个类的
java.lang.Class
对象,作为方法区中这个类的各种数据的访问入口。常用的.class
和getClass()
返回的就是这个对象。
javapublic class CustomLoader extends ClassLoader { @Override protected Class<?> findClass(String name) { // 1. 获取字节流(可来自文件/网络/数据库等) byte[] bytes = loadClassBytes(name); // 2. 转换为方法区数据结构 // 3. 创建Class对象 return defineClass(name, bytes, 0, bytes.length); } }
- 通过类的全限定名 (如
-
关键点:
-
由类加载器 完成。
-
加载的最终产物是堆中的
Class
对象。 -
类的加载是懒惰的,首次用到时才会加载:
- 使用了类.class
- 用类加载器的 loadClass 方法加载类
- 满足类的初始化条件(后文有详细介绍)
-
2.连接
2.1 验证
- 任务: 确保加载的字节码信息符合 JVM 规范,是安全、无害的,不会危害虚拟机自身安全。
- 内容:
- 文件格式验证(魔数、版本号等)
- 元数据验证(语义分析,如是否有父类、是否继承了final类、是否实现接口所有方法等)
- 字节码验证(数据流和控制流分析,确保逻辑正确,如操作数栈类型匹配、跳转指令目标合理等)
- 符号引用验证(检查常量池中的符号引用能否找到对应的类、字段、方法等)。
- 重要性: 保护JVM安全,防止恶意代码或损坏的字节码文件导致JVM崩溃或执行危险操作。虽然耗点时间,但对系统稳定性至关重要。
2.2 准备
- 任务: 为类的静态变量 分配内存,并设置默认初始值。
- 过程:
- 在方法区中为这些静态变量分配内存空间。
- 设置默认初始值:
- 基本类型:
int
->0
,long
->0L
,float
->0.0f
,double
->0.0d
,char
->'\u0000'
,boolean
->false
。 - 引用类型:
null
。
- 基本类型:
- 关键点:
- 这里分配内存并初始化的是类变量(static变量),不是实例变量。
- 初始化的值是默认零值,不是代码中显式赋的值 (如
public static int value = 123;
,在准备阶段后value
是0
,赋值123
的操作发生在后面的初始化阶段)。 - 如果静态变量是
final
修饰的基本类型或 String 常量 ,并且在编译时就能确定值(如public static final int CONSTANT = 100;
),那么这个值会直接在准备阶段被赋予(此时CONSTANT
就是100
)。
2.3 解析
- 任务: 将常量池内的符号引用 替换为直接引用。
- 符号引用与直接引用:
- 符号引用: 一组描述被引用目标(类、字段、方法)的符号。例如,
java/lang/Object
(类名)、toString:()Ljava/lang/String;
(方法名和描述符)。它只是一个字面量引用,与内存布局无关。 - 直接引用: 一个能直接定位到目标(类在方法区的地址、字段或方法在内存中的偏移量或句柄)的指针、偏移量或句柄。它是与JVM运行时内存布局相关的。
- 符号引用: 一组描述被引用目标(类、字段、方法)的符号。例如,
- 过程: JVM 查找符号引用所指向的类、字段或方法的实际位置,并将常量池中的符号引用替换为指向该位置的直接引用。
- 时机: 解析阶段可以在初始化之前完成,也可以在初始化之后完成(甚至延迟到第一次实际使用该符号引用时),这取决于 JVM 的实现策略("及早解析"或"惰性解析")。
3.初始化
- 任务: 执行类的初始化代码 ,主要是执行类构造器
<clinit>()
方法。 <clinit>()
方法:- 由编译器自动收集类中所有类变量(static变量)的显式赋值动作 和静态代码块(static {} 块) 中的语句合并生成。
- 顺序:按源代码中出现的顺序执行。
- 父类的
<clinit>()
优先于子类的执行。 - 虚拟机会保证一个类的
<clinit>()
方法在多线程环境下被正确地加锁、同步(即线程安全)。如果一个线程正在执行它,其他线程会阻塞等待。
- 触发时机(严格规定): 类只有在以下 6 种情况之一发生时,才会立即进行初始化(加载和连接可能更早发生):
- 创建类的实例 (
new
)。 - 访问类的静态变量(读取或赋值) ,除非该静态变量是
final
常量并且在编译期就能确定值(常量传播优化)。 - 调用类的静态方法 (
static
方法)。 - 使用反射 (
Class.forName("...")
,getMethod
等) 对类进行反射调用。 - 初始化一个类的子类时,会触发其父类的初始化。
- JVM 启动时被标明为启动类(包含
main()
方法的那个类)。
- 创建类的实例 (
- 关键点:
- 这是类加载过程的最后一步。
- 此时才真正执行程序员的代码逻辑(静态赋值、静态块)。
- 之前的"准备"阶段只是分配内存并赋零值,这里是赋程序员定义的值。
类加载器
在类加载的第一个阶段------加载中,JVM 需要根据类的全限定名,找到并读取其对应的字节码文件(.class
文件)。
这个查找和读取 .class
字节流的工作,正是由类加载器来完成的。

JVM 中内置了三个重要的 ClassLoader
:
- Bootstrap ClassLoader (启动类/引导类加载器):
- 用原生代码(C/C++)实现,是 JVM 自身的一部分。
- 负责加载
JAVA_HOME/lib
目录下的核心 Java 库(如rt.jar
,charsets.jar
)或-Xbootclasspath
参数指定的路径中的类。 - 是最高级别的加载器,没有父加载器。
null
表示: 在 Java 代码中试图获取它的引用时,返回null
。
- Extension ClassLoader (扩展类加载器):
- 由
sun.misc.Launcher$ExtClassLoader
实现(Java)。 - 负责加载
JAVA_HOME/lib/ext
目录下的扩展库,或java.ext.dirs
系统变量指定的路径中的所有类库。 - 其父加载器是 Bootstrap ClassLoader。
- 由
- Application ClassLoader (应用程序类加载器 / 系统类加载器):
- 由
sun.misc.Launcher$AppClassLoader
实现(Java)。 - 负责加载用户类路径(ClassPath) 上所指定的类库。这是我们程序中默认的类加载器。
- 其父加载器是 Extension ClassLoader。
- 通过
ClassLoader.getSystemClassLoader()
可以获取到它。
- 由
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求:
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader
抽象类。如果我们要自定义自己的类加载器,需要继承 ClassLoader
抽象类。
ClassLoader
类中有两个核心方法:
-
protected Class<?> loadClass(String name, boolean resolve)
加载指定名称的类。该方法实现了双亲委派模型 :会先委托给父加载器尝试加载,如果父加载器无法完成,才会调用自身的
findClass()
方法进行加载。javaprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先检查当前类是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 先让父类加载器尝试加载 c = parent.loadClass(name, false); } else { // 如果没有父加载器(即引导类加载器),使用 bootstrap 加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 忽略异常,进入下一步由自己加载 } if (c == null) { // 如果父加载器无法加载,再尝试使用当前类加载器加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
-
protected Class<?> findClass(String name)
根据类名查找类的定义并返回对应的
Class
对象。默认实现是抛出ClassNotFoundException
,需要我们在子类中重写。javaprotected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
双亲委派模型简介
双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载和防止核心 API 被篡改。
-
工作流程: 当一个类加载器收到加载类的请求时:
- 它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器。
- 只有当父加载器反馈自己无法完成这个加载请求 (在它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
-
核心思想: "向上委派,向下加载"。
-
核心目的:
- 保证基础类的唯一性和安全性: 防止用户自定义一个与核心库(如
java.lang.Object
)同名的类被加载,从而覆盖核心库的行为(沙箱安全机制)。 - 避免重复加载: 父加载器已经加载过的类,子加载器就不会再加载(在同一个命名空间内)。
- 保证基础类的唯一性和安全性: 防止用户自定义一个与核心库(如