当前位置: 首页 > 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.lryc.cn/news/622763.html

相关文章:

  • LintCode第116题-跳跃游戏
  • java项目怎么实现用户行为分析、漏斗转化、数据可视化报表。
  • 【Linux系统】进程间通信:System V IPC——共享内存
  • FPGA实现I2C通信方案
  • 创建maven module中的override
  • 库的制作与原理
  • Navicat 为 SQLite 数据库设置密码指南
  • 如何使用 Git 修改已推送 Commit 的用户名和邮箱
  • 从废弃到珍宝——旧物二手回收小程序系统的价值发现之旅
  • 配置 Docker 镜像加速,解决 docker pull 拉取镜像失败、docker search 查询镜像失败等问题
  • 外出业务员手机自动添加报价单​——仙盟创梦IDE
  • PostgreSQL——事务处理与并发控制
  • 关于casdoor重定向问题
  • 力扣(最小覆盖子串)
  • Java设计模式之《工厂模式》
  • 【Java web】HTTP 协议详解
  • PO BO VO DTO POJO DAO DO概念
  • Linux第十四讲:网络基础概念
  • Jenkins Pipeline中参数化构建
  • Android 移动端 UI 设计:前端常用设计原则总结
  • 后台管理系统-3-vue3之左侧菜单栏和头部导航栏的静态搭建
  • flowable汇总查询方式
  • SAP-FI配置与业务解析之内部交易核算
  • 双向SSL认证之Apache实战配置
  • 3 种方式玩转网络继电器!W55MH32 实现网页 + 阿里云 + 本地控制互通
  • 数据清洗与机器学习贷款偿还预测建模
  • (职业分析)讨好型人格适合什么职业?
  • 【LLM微调】
  • 每日算法刷题Day62:8.16:leetcode 堆8道题,用时2h30min
  • java项目中什么时候使用static、final