【JVM】类初始化和加载

一、对象的创建过程

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角度看一个对象已经创建完毕。接着执行实例初始化

  1. 初始化字段:按照代码顺序执行字段的显式初始化
  2. 执行实例代码块 :编译器会将所有非静态代码块收集并插入到构造器开头
  3. 执行构造器代码:执行开发者编写的构造函数代码

至此,一个完整的、可用的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)

类加载的最后阶段,执行类的初始化代码。

触发时机(以下情况之一):

  1. 创建类的实例(new
  2. 访问类的静态变量(非final)
  3. 调用类的静态方法
  4. 使用反射(Class.forName()
  5. 初始化子类时,父类未初始化
  6. 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("静态代码块")
}

初始化顺序

  1. 父类静态变量和静态代码块
  2. 子类静态变量和静态代码块
  3. 父类实例变量和实例代码块
  4. 父类构造器
  5. 子类实例变量和实例代码块
  6. 子类构造器
4. 使用(Using)

类完成加载后进入使用阶段,程序可以:

  • 创建类的实例对象
  • 访问类的静态成员
  • 调用类的方法
  • 使用反射操作类
5. 卸载(Unloading)

当类不再被需要时,JVM会将其从内存中卸载。

卸载条件(同时满足):

  1. 实例全部回收:该类所有的实例都已被垃圾回收
  2. ClassLoader回收:加载该类的ClassLoader已被回收
  3. Class对象无引用:该类的Class对象没有被任何地方引用

卸载过程

java 复制代码
// 假设场景:Web应用卸载
WebappClassLoader loader = new WebappClassLoader();
Class<?> clazz = loader.loadClass("com.example.MyServlet");

// 当Web应用停止时:
// 1. 销毁所有MyServlet实例 → 实例回收 ✓
// 2. 回收WebappClassLoader → ClassLoader回收 ✓  
// 3. 清除对Class对象的引用 → 无引用 ✓
// 结果:MyServlet类被卸载

注意事项

  1. 启动类加载器加载的类通常不会被卸载
  2. 自定义类加载器加载的类在适当条件下可被卸载
  3. 类的卸载有助于减少元空间内存占用

📌 完整类加载流程图

复制代码
开始
  ↓
[加载阶段]
  ├── 读取.class文件
  ├── 转换运行时结构
  ├── 创建Class对象
  └── 建立方法区访问入口
  ↓
[链接阶段]
  ├── [验证]
  │   ├── 文件格式验证
  │   ├── 元数据验证  
  │   ├── 字节码验证
  │   └── 符号引用验证
  │
  ├── [准备]
  │   ├── 分配静态变量内存
  │   ├── 设置默认值/实际值
  │   └── 处理static final常量
  │
  └── [解析]
      ├── 类/接口解析
      ├── 字段解析
      └── 方法解析
      ↓
[初始化阶段]
  ├── 执行<clinit>()方法
  ├── 初始化静态变量
  └── 执行静态代码块
  ↓
[使用阶段]
  ├── 创建实例
  ├── 访问成员
  └── 调用方法
  ↓
[卸载阶段](条件满足时)
  ├── 回收所有实例
  ├── 回收ClassLoader
  └── 清除Class引用
  ↓
结束

关键要点总结

  1. 类加载是懒加载的:只有真正使用时才会触发初始化
  2. 准备阶段只处理静态变量:实例变量在对象创建时初始化
  3. 解析将符号变直接:从描述性引用到具体内存地址
  4. 初始化执行clinit():收集所有静态初始化代码
  5. 卸载条件苛刻:需要实例、ClassLoader、Class对象都符合条件
  6. 双亲委派保护核心:防止核心类被篡改,确保类唯一性
相关推荐
鱼跃鹰飞4 小时前
设计模式系列:工厂模式
java·设计模式·系统架构
时见先生4 小时前
Python库和conda搭建虚拟环境
开发语言·人工智能·python·自然语言处理·conda
a努力。4 小时前
国家电网Java面试被问:混沌工程在分布式系统中的应用
java·开发语言·数据库·git·mysql·面试·职场和发展
Yvonne爱编码4 小时前
Java 四大内部类全解析:从设计本质到实战应用
java·开发语言·python
wqwqweee5 小时前
Flutter for OpenHarmony 看书管理记录App实战:搜索功能实现
开发语言·javascript·python·flutter·harmonyos
J2虾虾5 小时前
SpringBoot和mybatis Plus不兼容报错的问题
java·spring boot·mybatis
yongui478345 小时前
基于MATLAB的NALM锁模光纤激光器仿真实现
开发语言·matlab
毕设源码-郭学长5 小时前
【开题答辩全过程】以 基于springboot 的豪华婚车租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
-To be number.wan6 小时前
Python数据分析:numpy数值计算基础
开发语言·python·数据分析
Cx330❀7 小时前
【优选算法必刷100题】第038题(位运算):消失的两个数字
开发语言·c++·算法·leetcode·面试