当前位置: 首页 > news >正文

类的生命周期与加载过程

类加载的 5 个阶段:逐阶段拆解

一、加载(Loading):找到 class 文件,变成内存中的 “类模板”

加载是类加载过程的第一步,核心是将类的二进制数据(class 文件)加载到内存,并生成一个代表该类的Class对象(这个对象是类的 “模板”,存放在方法区)。

  • 具体操作
    1. 定位 class 文件:JVM 通过类的 “全限定名”(比如com.test.User)找到对应的 class 文件。来源可能有:
      • 本地硬盘(最常见,比如项目的target/classes目录);
      • 网络(比如从远程服务器下载);
      • 动态生成(比如通过ASM框架在运行时生成);
      • 数据库或压缩包(比如从jar包中读取)。
    1. 读取二进制数据:把 class 文件的二进制字节流读入内存。
    2. 生成 Class 对象:将字节流转换成方法区中的 “运行时数据结构”(类的元数据,比如类的属性、方法、父类等信息),并在堆中生成一个对应这个类的Class对象(作为方法区中元数据的访问入口)。
  • 例子
    当我们第一次执行User u = new User();时,JVM 会先委托类加载器(比如应用类加载器)去查找com/test/User.class文件,找到后读入内存,生成Class<User>对象。这个Class对象就像 “用户模板”,后续创建User对象都要基于它。
  • 谁来执行?:由类加载器(ClassLoader)完成,JVM 中有三种类加载器(双亲委派模型):
    • 启动类加载器(Bootstrap ClassLoader):加载 JDK 核心类(比如java.lang.String);
    • 扩展类加载器(Extension ClassLoader):加载 JDK 扩展目录的类;
    • 应用类加载器(Application ClassLoader):加载我们自己写的类。
二、验证(Verification):确保 class 文件 “合法安全”

验证是为了防止恶意或无效的 class 文件破坏 JVM,相当于给 class 文件做 “全面体检”,主要包括 4 种验证:

  1. 文件格式验证
    检查 class 文件的二进制格式是否正确,比如:
    • 开头是否有 “魔数”(0xCAFEBABE,class 文件的标识,就像身份证的防伪标志);
    • 版本号是否符合当前 JVM 的要求(比如高版本 JDK 编译的 class 文件不能在低版本 JVM 上运行);
    • 常量池的格式是否正确等。
  1. 元数据验证
    检查类的元数据(比如类的结构信息)是否符合 Java 语言规范,比如:
    • 类是否有父类(除了java.lang.Object,所有类都必须有父类);
    • 类是否继承了不允许被继承的类(比如被final修饰的类);
    • 方法参数是否合法等。
  1. 字节码验证
    最复杂的一步,检查方法体的字节码是否安全,比如:
    • 确保指令不会跳转到方法体外;
    • 操作栈的数据类型和指令匹配(比如不能用int类型的指令操作long类型的数据);
    • 不会出现空指针访问等。
  1. 符号引用验证
    检查类中引用的其他类、方法、字段是否存在且可访问,比如:
    • 引用的类是否存在;
    • 调用的方法是否在该类中存在,参数是否匹配;
    • 是否有权限访问(比如不能访问private方法)。

  • 例子
    如果有人手动修改 class 文件,把开头的 “魔数” 改成其他值,文件格式验证就会失败,JVM 会直接抛出异常,拒绝加载这个类。
三、准备(Preparation):给静态变量 “分配内存并设默认值”

准备阶段的核心是为类的静态变量(类变量)分配内存,并设置 “默认初始值”(不是代码中定义的初始值)。

  • 关键细节
    1. 只处理静态变量:实例变量(非静态变量)的内存分配在创建对象时进行,这里不处理。
    2. 默认初始值:根据变量类型设置 JVM 默认值,比如:
      • int → 0;long → 0L;float → 0.0f;
      • boolean → false;
      • 引用类型(比如String)→ null。
    1. 内存位置:静态变量的内存分配在方法区(JDK8 及以后是元空间)。
  • 例子
    代码中定义:public static int count = 100;
    准备阶段会为count分配内存,设置默认值 0(不是 100);100这个值会在后面的 “初始化” 阶段设置。
  • 特殊情况
    如果静态变量被final修饰(常量),比如public static final int MAX = 200;,准备阶段会直接设置为 200(而不是默认值 0),因为final常量在编译时就已确定值,会被放入常量池。
四、解析(Resolution):把 “符号引用” 换成 “直接引用”

解析阶段的作用是将常量池中的 “符号引用” 替换为 “直接引用”

  • 先理解两个概念
    • 符号引用:用字符串描述的引用,比如代码中写User.name,编译后在 class 文件中存的是 “User类的name字段” 这个字符串标识,不涉及具体内存地址。
    • 直接引用:指向内存中实际对象的地址(比如指针、偏移量),JVM 可以通过直接引用直接找到目标。
  • 解析的内容
    主要对类、接口、字段、方法、接口方法的符号引用进行解析,比如:
    • 把 “com.test.User” 这个类的符号引用,换成方法区中User类元数据的实际地址;
    • 把 “User.name” 这个字段的符号引用,换成User类中name字段在方法区的实际内存偏移量。
  • 例子
    当代码中调用User.sayHello()时,编译后 class 文件中存的是 “User类的sayHello方法” 这个符号引用;解析阶段会把它换成sayHello方法在内存中的实际地址,这样 JVM 执行时就能直接找到该方法的字节码。
  • 注意:解析阶段不一定在准备阶段后马上执行,也可能在初始化阶段之后(动态解析),比如当遇到动态绑定(多态)时,会延迟到运行时再解析。
五、初始化(Initialization):执行静态代码,给静态变量赋 “实际值”

初始化是类加载过程的最后一步,也是真正执行类中定义的 Java 代码的阶段,核心是执行 “类构造器<clinit>()方法”。

  • <clinit>()方法是什么?
    它是 JVM 自动生成的,由类中所有静态变量的赋值语句静态代码块按顺序合并而成。比如:java运行
public class User {public static int a = 1; // 静态变量赋值static { // 静态代码块a = 2;System.out.println("静态代码块执行");}public static int b = 3; // 静态变量赋值
}


JVM 会生成<clinit>()方法,执行顺序是:a=1 → 执行静态代码块(a=2)→ b=3

  • 初始化的触发时机(主动使用)
    只有当类被 “主动使用” 时,才会触发初始化(JVM 规范严格规定),包括:
    1. 创建类的实例(new User());
    2. 调用类的静态方法(User.method());
    3. 访问类的静态变量(User.a,但被final修饰的常量除外,因为它在准备阶段已赋值);
    4. 反射调用(Class.forName("com.test.User"));
    5. 初始化子类时,父类未初始化则先初始化父类;
    6. 启动类(包含main方法的类)会被初始化。
  • 不触发初始化的情况(被动使用)
    • 访问类的static final常量(比如User.MAX,因为准备阶段已赋值);
    • 通过子类访问父类的静态变量(只会初始化父类,不初始化子类);
    • 创建数组(User[] users = new User[10],这只是创建数组对象,不会初始化User类)。
  • 例子
    执行User u = new User();时,会触发User类的初始化:
    1. 执行<clinit>()方法,给a赋 1→2,b赋 3,打印 “静态代码块执行”;
    2. 之后才会创建User实例(执行实例构造器<init>()方法)。

总结:用 “做饭” 类比类加载过程

  • 加载:把菜谱(class 文件)从书架(硬盘)拿到厨房(内存);
  • 验证:检查菜谱是否完整、有没有错误(比如步骤是否矛盾);
  • 准备:给厨房的调料罐(静态变量)贴上标签,先装默认的水(默认值);
  • 解析:把菜谱中 “隔壁老王的酱油”(符号引用)换成具体的位置(比如 “厨房第三层左数第一个瓶子”);
  • 初始化:按照菜谱的开头步骤(静态代码块和静态变量赋值),把水倒掉,装上真正的酱油、醋(赋实际值)。

http://www.dtcms.com/a/335161.html

相关文章:

  • 地理信息系统教程(汤国安老师书)—— 第二章课后习题
  • 掌握提示词工程:让大模型更懂你的需求
  • vue:vue3 watch 属性
  • Nacos Server 3.0.x安装教程
  • JAVA面试汇总(四)JVM(一)
  • 软件包管理-源代码安装
  • GaussDB 数据库架构师修炼(十三)安全管理(4)-数据库审计
  • Win11更新0x80073712错误解决方法
  • 优雅草星云物联网项目私有化定制技术解析:RS485接口与工业通讯协议-优雅草卓伊凡
  • 初识c语言————宏定义和调用
  • SpringSecurity(一)入门
  • 行为型设计模式:对象协作的舞蹈家(上)
  • 车行横洞*到底是什么
  • 原码表示法、反码表示法、移码表示法、补码表示法
  • C++自旋锁的后退机制简介
  • 初学python的我开始Leetcode题15-3
  • 从0开始学习Java+AI知识点总结-16.web基础知识
  • [ai-agent]环境简介之沙盒e2b vs daytona
  • 深入解析 @nestjs/typeorm的 forRoot 与 forFeature
  • 新手向:GitCode疑难问题诊疗
  • 搜索算法 (一)- 深度优先和广度优先
  • “openfeign“ 报错Invalid bound statement (not found)
  • windows开机启动软件
  • 低空经济产业链全景解析
  • ISIS区域内、区域间计算
  • 发文暴论!线性注意力is all you need!
  • Windows 操作系统 - Windows 恢复浏览器标题栏颜色
  • VS Code配置MinGW64编译Ipopt库
  • 什么是微前端?
  • 关键点检测(11)-HRNet网络