类加载过程:Loading
类加载:把字节码变成可用的 Class 对象
当程序第一次用到某个类时——比如 new Student()、Class.forName("Student")、或者访问 Student.class——JVM 需要先把 Student.class 文件加载进内存,变成一个 Class 对象,才能使用它。
这个过程就是类加载(Class Loading)。
类加载的完整生命周期
类从加载到卸载,经历以下阶段:
加载(Loading)
│
▼
链接(Linking)
├─ 验证(Verification)
├─ 准备(Preparation)
└─ 解析(Resolution)
│
▼
初始化(Initialization)
│
▼
使用(Using)
│
▼
卸载(Unloading)其中加载、验证、准备、初始化、卸载是确定的执行顺序,而解析可以在初始化之后再进行(晚期绑定)。
Loading:加载阶段做了什么
加载阶段的核心任务是:通过类的全限定名找到字节码文件,并将其转换为方法区中的运行时数据结构,同时在堆中生成一个 java.lang.Class 对象作为访问入口。
三件事
- 通过类的全限定名获取字节码
类加载器根据类的全限定名(如 com.example.Student)到特定位置查找 .class 文件。
不同类加载器的查找路径不同:
| 类加载器 | 加载路径 |
|---|---|
| 引导类加载器 | JAVA_HOME/jre/lib/rt.jar 等核心 JAR |
| 扩展类加载器 | JAVA_HOME/jre/lib/ext/*.jar |
| 应用类加载器 | classpath(java -cp 或 -jar 指定) |
- 将字节流转换为方法区的运行时数据结构
字节码文件(Class 文件)是一个二进制流。加载时,JVM 解析这个二进制流,在方法区中创建类的内部表示结构:
ClassFile 二进制流
↓
解析
↓
方法区中的类结构
├─ 类的元数据(名字、父类、修饰符)
├─ 字段表(有哪些属性)
├─ 方法表(有哪些方法,包含字节码)
└─ 常量池(各种符号引用)- 在堆中生成 Class 对象
这是关键一步:加载完成后,JVM 会在堆内存中创建一个 java.lang.Class 对象。Java 程序通过这个 Class 对象来访问方法区中的类数据。
public class LoadingDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 三种获取 Class 对象的方式
// 方式一:通过类的 class 属性
Class<String> c1 = String.class;
// 方式二:通过对象的 getClass() 方法
String s = "hello";
Class<?> c2 = s.getClass();
// 方式三:通过 Class.forName() 触发加载
Class<?> c3 = Class.forName("java.lang.String");
// 三种方式拿到的是同一个 Class 对象
System.out.println(c1 == c2); // true
System.out.println(c2 == c3); // true
}
}注意:
Class对象在堆中,类的内部数据(字节码、元数据)在方法区中。
数组类的加载
数组类的加载比较特殊,因为它没有字节码文件。数组类由 JVM 直接在内存中构造。
public class ArrayLoading {
public static void main(String[] args) {
// 数组类型不需要 ClassLoader 加载
int[] arr1 = new int[10];
String[] arr2 = new String[10];
// 数组类的 Class 对象
System.out.println(arr1.getClass()); // class [I
System.out.println(arr2.getClass()); // class [Ljava.lang.String;
// [I 表示 int[]
// [Ljava.lang.String; 表示 String[]
// [[[I 表示 int[][][]
}
}数组类的加载规则:
- 如果数组的组件类型(去掉一层数组后的类型)是引用类型,数组类由加载该组件类型的类加载器加载
- 如果数组的组件类型是基本类型(如
int[]),数组类不由任何类加载器加载,而是由 JVM 直接创建
加载阶段与命名空间
每个类加载器都有自己独立的命名空间:由该类加载器及其所有父加载器所加载的类的集合。
public class NamingSpace {
public static void main(String[] throws Exception) {
// 自定义类加载器
ClassLoader myLoader = new MyClassLoader();
// 用自定义加载器加载一个类
Class<?> clazz = myLoader.loadClass("com.example.Test");
// 用系统类加载器尝试加载同一个类
Class<?> systemClazz = Class.forName("com.example.Test");
// 结果:不是同一个类!
System.out.println(clazz == systemClazz); // false
}
}同一个类(同一个全限定名)可以被不同类加载器加载,它们在 JVM 中是两个不同的类。这在 OSGi、热部署等场景下是核心机制。
加载的结束时机
一个类在以下情况下会被完全加载:
- 当字节码被成功读取、解析并创建了
Class对象 - 当所有相关的类变量(
static字段)和静态代码块(static {})都准备好
注意:加载和初始化是不同的阶段。加载完成不代表初始化完成。关于初始化的详细规则,我们会在 类加载过程:Initialization 中深入讲解。
加载与验证的关系
很多人混淆加载和验证。简单说:
- 加载:把字节码从文件读取到内存
- 验证:检查读取进来的字节码是否合法
它们是链接(Linking)阶段的第一步,可以理解为:加载过程的后半段会穿插进行验证。
本节小结
加载阶段的核心任务:
- 通过全限定名定位字节码文件
- 将字节码转换为方法区的运行时数据结构
- 在堆中创建
Class对象,作为程序访问类数据的入口
理解加载阶段的关键是记住:类信息存储在方法区,但 Class 对象存在于堆中。
下一节,我们来看加载之后发生的事情——类加载过程:Linking(验证/准备/解析)。
