一、什么是类加载(Class Loading)?
在 Java 程序运行时,类(.class 文件)并不是一次性全部加载进 JVM 的,而是 按需动态加载 的。这个过程由 类加载器(ClassLoader) 完成(我们应该知道类加载器的结构,是各自完成自己负责的类的加载任务),它的任务是:
把类的字节码文件加载到 JVM 内存中,并在方法区(元空间)中创建对应的类元数据结构。
二、类加载的七个阶段(从加载到使用)
JVM 规范定义了类的完整生命周期:
加载(Loading)
→ 连接(Linking)
├ 验证(Verification)
├ 准备(Preparation)
└ 解析(Resolution)
→ 初始化(Initialization)
→ 使用(Using)
→ 卸载(Unloading)
我们主要分析前四步,因为 它们决定类是如何进入内存、被准备好使用的。
① 加载(Loading)
目标:
-
通过类的全限定名(如
java.lang.String)获取其字节码文件; -
将字节码读入内存;
-
在 元空间(Metaspace) 中为该类生成对应的 类元数据结构(Class对象的内部描述);
-
生成一个
java.lang.Class实例(代表该类的"镜像")。
加载的来源可能是:
-
本地文件系统(
.class或.jar) -
网络(如 Web Start)
-
动态生成(如反射、动态代理、ASM 字节码)
类加载器(ClassLoader)机制:
-
启动类加载器(BootstrapClassLoader) :加载 JDK 核心类库(
rt.jar/java.base)。 -
扩展类加载器(ExtClassLoader) :加载
jre/lib/ext目录下的类。 -
应用类加载器(AppClassLoader) :加载
classpath下的应用类。 -
自定义类加载器:由开发者自定义加载路径或加密逻辑。
双亲委派模型:
加载请求会先向上委派,父加载器若无法加载,才由子加载器尝试,防止核心类被篡改。
此时开始涉及元空间(Metaspace)!
类加载器加载类的字节码文件的过程中,将这个类的相关信息放入元空间
-
类的元数据(字段表、方法表、常量池等)被加载到 元空间(方法区实现);
-
但类的静态变量等数据还未初始化。(在准备阶段赋值)
② 连接(Linking)
连接阶段将类加载后的字节码转为可执行的 JVM 运行时结构。
(1)验证(Verification)(先保证安全)
确保字节码合法、安全、不破坏 JVM。
-
格式验证:魔数、版本号是否正确;
-
元数据验证:父类存在吗?接口合法吗?
-
字节码验证:方法体的控制流、操作数栈是否合法;
-
符号引用验证:常量池中的引用类型正确。
如果验证失败,会抛出 VerifyError。
(2)准备(Preparation)
为类的 静态变量分配内存并设为默认值(不执行赋值语句,但是final修饰的变量会在这里就进行赋值,特例:final x = new object() 这样的语句执行情况不会因为final修饰而提前赋值,因为要new 创建一个对象,所以不能现在赋值而是出现在类的构造方法中)
这些静态变量存放在 Java 堆(Heap) 或 元空间的静态区域中 (取决于实现),
但变量的"定义信息"仍在元空间。
(3)解析(Resolution)
把常量池中的 符号引用 转换为 直接引用(真正与虚拟机内存关联起来,作为符号引用时,读取的时候并不知道是位于内存的哪个位置,但是解析之后就能够通过引用真正找到他在内存中的位置)。
③ 初始化(Initialization)
真正执行类的 <clinit>() 方法(类初始化块 + 静态变量初始化)。
前面准备的时候为静态变量赋默认值,但是现在就是真正执行java代码了:
public static int a = 10;
static {
a = 20;
}
这个阶段执行的就是:
-
静态变量赋值语句;
-
static {}静态代码块; -
初始化顺序:父类先于子类。
只有初始化阶段才真正执行 Java 代码(前面阶段都不执行代码)。
那么类的初始化会在什么时候发生呢?
概括得说,类初始化是【懒情的】
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时(初始化之前的阶段只对静态变量赋默认值,如果要访问,那么就应该赋真正值,所以应该快速完成初始化(初始化的时候真正执行代码赋值))
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new会导致初始化
不会触发初始化的情况:
- 访问类的staticfinal静态常量(基本类型和字符串)不会触发初始化(因为他们在链接的准备阶段就赋值好了)
- 类对象.class不会触发初始化(因为加载阶段就已经生成好miror.class了)
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass方法(classLoader发生在类加载阶段)
- Class.forName的参数2(initialize)为 false时
这里有一个小细节:对于下面三种情况,真正用到了三个final修饰的值,会不会导致类执行初始化?
①不会,因为准备阶段直接赋值了;
②会不会? 不会,因为虽然final修饰引用类型,但是用的是字面量,所以"hello"存放在串池中,没有实际意义上进行new一个对象,所以不会导致类初始化;
③会,因为Integer是包装类,引用类型

④ 使用(Using)
类初始化完成后,就可以:
-
创建实例;
-
调用静态方法;
-
访问静态变量;
-
进行反射操作;
-
被其他类继承。
⑤ 卸载(Unloading)
当:
-
该类对应的
ClassLoader被 GC 回收; -
且没有该类的实例对象存在;
-
且没有该类的
Class对象引用;
则该类的元数据会从 元空间(Metaspace) 中释放。
最后,类的相关信息(类结构、方法表、常量池等)都被存储在元空间中,但是类的对象存储在java堆中。
细节如下:
-
加载阶段 将类的元数据加载进 元空间(Metaspace);
-
验证-准备-解析;
-
初始化阶段 才真正执行静态代码;
-
类的元信息(字段表、方法表等)存放在 Metaspace;
-
之后就可以使用这个类了,创建类的实例,通过实例调用方法,但是要知道类对象在堆中;
-
类的实例对象存放在 Heap;
-
类被卸载时,对应的 元空间内存 也会被释放。