一、对象的创建过程
1. 类加载检查
当尝试创建一个对象时,JVM首先会到元空间 中查找该对象的类符号引用 (可以理解为类的模板信息)。检查这个类是否已经被加载、解析和初始化 。如果没有,则会触发完整的类加载过程。
2. 分配内存
类加载检查通过后,JVM会在堆内存中为新对象划分一块内存空间。具体的内存分配方式取决于垃圾收集器的实现,常见的有:
- 指针碰撞:适用于内存规整的GC算法(如Serial、ParNew)
- 空闲列表:适用于内存不规整的GC算法(如CMS)
3. 初始化零值
内存分配完成后,JVM会将对象的所有实例字段 初始化为对应类型的零值:
- 数值类型(int、long等):
0 - 布尔类型:
false - 引用类型:
null
此操作确保了对象字段在使用前都有确定的初始值,这是Java内存安全的重要保障。
4. 设置对象头
接下来设置对象的对象头(Object Header),主要包括:
- Mark Word :存储对象的运行时数据
- 哈希码(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁等
- Klass Pointer :指向方法区中的Class对象,确定对象的类型
- 如果是数组对象,还会包含数组长度信息
5. 执行init方法
上述步骤完成后,从JVM角度看一个对象已经创建完毕。接着执行实例初始化:
- 初始化字段:按照代码顺序执行字段的显式初始化
- 执行实例代码块 :编译器会将所有非静态代码块收集并插入到构造器开头
- 执行构造器代码:执行开发者编写的构造函数代码
至此,一个完整的、可用的Java对象才真正创建完成。
| 阶段 | 执行内容 | 涉及内存区域 |
|---|---|---|
| 类加载检查 | 查找类符号引用,必要时加载类 | 元空间 |
| 分配内存 | 在堆中划分对象空间 | 堆 |
| 初始化零值 | 设置字段默认值(0、false、null) | 堆 |
| 设置对象头 | 设置对象元数据(Mark Word、类型指针) | 堆 |
| 执行init方法 | 执行代码块和构造器,完成对象初始化 | 堆 |
二、类加载过程
类加载是JVM将类的字节码文件加载到内存,并进行验证、准备、解析和初始化的完整过程。整个过程分为三大阶段:加载 → 链接 → 初始化,其中链接阶段又包含验证、准备、解析三个子阶段。
1. 加载(Loading)
核心任务:将类的字节码数据加载到内存中,并创建对应的Class对象。
具体过程:
- 读取字节码 :通过类的全限定名 获取对应的字节码文件(
.class) - 二进制转换:将字节码转换为方法区中的运行时数据结构
- 创建Class对象 :在堆 中生成一个代表该类的
java.lang.Class对象 - 建立访问入口:Class对象作为程序访问方法区中类元数据的入口
2. 链接(Linking)
链接阶段负责将加载到内存的类数据进行整合和准备。
2.1 验证(Verification)
确保被加载的类符合JVM规范,不会危害虚拟机安全。
四个验证阶段:
| 验证阶段 | 验证内容 | 重要性 |
|---|---|---|
| 文件格式验证 | 魔数、版本号、常量池等 | 确保.class文件格式正确 |
| 元数据验证 | 类的继承关系、字段/方法访问性 | 确保语义符合Java规范 |
| 字节码验证 | 方法体、栈帧、类型转换等 | 确保程序逻辑正确性 |
| 符号引用验证 | 符号引用的合法性 | 确保解析能正常进行 |
2.2 准备(Preparation)
为类的静态变量分配内存并设置初始值。
关键规则:
java
// 情况1:普通static变量(赋默认值)
public static int count; // 准备阶段:count = 0
public static String name; // 准备阶段:name = null
// 情况2:static final编译时常量(直接赋实际值)
public static final int MAX = 100; // 准备阶段:MAX = 100
public static final String TITLE = "App"; // 准备阶段:TITLE = "App"引用
// 情况3:static final非编译时常量(赋默认值)
public static final Date NOW = new Date(); // 准备阶段:NOW = null
内存分配:
- 静态变量在方法区(元空间) 分配内存
- 基本类型分配固定大小,引用类型分配引用空间
2.3 解析(Resolution)
将常量池中的符号引用 替换为直接引用。
| 引用类型 | 符号引用(解析前) | 直接引用(解析后) |
|---|---|---|
| 类/接口 | java/lang/String |
Class对象内存地址 |
| 字段 | java/lang/System.out |
字段内存偏移量 |
| 方法 | java/io/PrintStream.println |
方法入口地址或方法表索引 |
解析时机:
- 积极解析:类加载时就解析所有符号引用(默认)
- 惰性解析:第一次使用时才解析(某些JVM实现)
3. 初始化(Initialization)
类加载的最后阶段,执行类的初始化代码。
触发时机(以下情况之一):
- 创建类的实例(
new) - 访问类的静态变量(非final)
- 调用类的静态方法
- 使用反射(
Class.forName()) - 初始化子类时,父类未初始化
- JVM启动时指定的主类
执行内容 :执行<clinit>()方法(类构造器)
java
public class Example {
// 静态变量赋值(收集到<clinit>())
public static int a = initA();
// 静态代码块(收集到<clinit>())
static {
System.out.println("静态代码块");
}
private static int initA() {
return 100;
}
// <clinit>()方法包含:
// 1. a = initA()
// 2. System.out.println("静态代码块")
}
初始化顺序:
- 父类静态变量和静态代码块
- 子类静态变量和静态代码块
- 父类实例变量和实例代码块
- 父类构造器
- 子类实例变量和实例代码块
- 子类构造器
4. 使用(Using)
类完成加载后进入使用阶段,程序可以:
- 创建类的实例对象
- 访问类的静态成员
- 调用类的方法
- 使用反射操作类
5. 卸载(Unloading)
当类不再被需要时,JVM会将其从内存中卸载。
卸载条件(同时满足):
- 实例全部回收:该类所有的实例都已被垃圾回收
- ClassLoader回收:加载该类的ClassLoader已被回收
- Class对象无引用:该类的Class对象没有被任何地方引用
卸载过程:
java
// 假设场景:Web应用卸载
WebappClassLoader loader = new WebappClassLoader();
Class<?> clazz = loader.loadClass("com.example.MyServlet");
// 当Web应用停止时:
// 1. 销毁所有MyServlet实例 → 实例回收 ✓
// 2. 回收WebappClassLoader → ClassLoader回收 ✓
// 3. 清除对Class对象的引用 → 无引用 ✓
// 结果:MyServlet类被卸载
注意事项:
- 由启动类加载器加载的类通常不会被卸载
- 自定义类加载器加载的类在适当条件下可被卸载
- 类的卸载有助于减少元空间内存占用
📌 完整类加载流程图
开始
↓
[加载阶段]
├── 读取.class文件
├── 转换运行时结构
├── 创建Class对象
└── 建立方法区访问入口
↓
[链接阶段]
├── [验证]
│ ├── 文件格式验证
│ ├── 元数据验证
│ ├── 字节码验证
│ └── 符号引用验证
│
├── [准备]
│ ├── 分配静态变量内存
│ ├── 设置默认值/实际值
│ └── 处理static final常量
│
└── [解析]
├── 类/接口解析
├── 字段解析
└── 方法解析
↓
[初始化阶段]
├── 执行<clinit>()方法
├── 初始化静态变量
└── 执行静态代码块
↓
[使用阶段]
├── 创建实例
├── 访问成员
└── 调用方法
↓
[卸载阶段](条件满足时)
├── 回收所有实例
├── 回收ClassLoader
└── 清除Class引用
↓
结束
关键要点总结
- 类加载是懒加载的:只有真正使用时才会触发初始化
- 准备阶段只处理静态变量:实例变量在对象创建时初始化
- 解析将符号变直接:从描述性引用到具体内存地址
- 初始化执行clinit():收集所有静态初始化代码
- 卸载条件苛刻:需要实例、ClassLoader、Class对象都符合条件
- 双亲委派保护核心:防止核心类被篡改,确保类唯一性