JVM第三步——class文件如何进入虚拟机

本文详细介绍了JVM的类加载机制,包括加载、验证、准备、解析和初始化五个阶段。当遇到特定场景,如创建对象、访问静态字段、调用静态方法等时,JVM会进行类的初始化。加载阶段主要任务是获取类的二进制字节流,验证阶段则是为了确保字节流的合法性。准备阶段为类变量分配内存并设置初始值,解析阶段将符号引用替换为直接引用,而初始化阶段执行类构造器clinit方法,按程序员的意愿初始化类变量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

class文件描述的各种信息需要加载到虚拟机才能运行和使用,将class文件转移到虚拟机中你需要知道它是如何转移,又转移到哪里。此处记录的是class文件如何进入虚拟机,class文件只有加载进虚拟机后才能使用。

虚拟机的类加载机制

将描述类的信息的class文件加载到内存,并对数据进行校验、转换解析和初始化,形成虚拟机直接使用的Java类型。
Java语言在程序运行时对类型进行加载、连接、初始化。Java语言的动态扩展特性依靠运行期动态加载和动态连接。
类被加载到虚拟机内存中去和从内存中卸载出经历的生命周期为:加载、验证、准备、解析、初始化、使用、卸载这七个阶段。其中验证、准备、解析三个阶段统称为连接。加载、验证、准备、初始化和卸载必须有这种相对顺序,解析阶段可以在初始化之后再开始。

何时开始加载阶段呢?

或者说何时进行初始化阶段 简单说就是使用时必须要初始化
Java虚拟机并无规定何时开始加载阶段,但是严格规定如下5种(有且仅有)情况必须立即进行“初始化”阶段。按照严格的相对顺序,此时必须完成加载、验证、准备这三个阶段。

  • new(实例化对象的时候)、putstatic+getstatic(设置或者获得类的静态字段)、invokestatic(调用静态方法时),如未曾初始化该类需要先初始化。
  • 对类进行反射调用,但该类没有进行初始化。
  • 初始化类时发现其父类并未初始化
  • 首先初始化用户在虚拟机启动时指定的主类。
  • 动态使用的类未曾初始化。

类加载的全过程如下

加载阶段 --给JVM提供“原材料”

要完成如下三件事情:

  • 通过类的全限名获取该类的二进制字节流(可以理解为class文件,但是不止class文件)
    • class文件
    • jsp应用等其他文件生成
    • 从网络中获取
  • 将字节流中的静态存储结构转换成方法区的运行时数据结构
  • 生成代表该类的引用(Class类型的对象),这是这个类在方法区所有数据的访问入口。 class类型的对象虽然是对象,但是却存在于方法区中。
  • 加载阶段的开发人员可控性强,可以自定义类加载器(可以控制字节流的获取方式)。类和该类的类加载器一起确定唯一性
  • 类加载阶段完成后,JVM外部的二进制流就按照格式存储在方法区中了。
验证阶段 --自我保护的手段
  • 验证阶段的主要目的:确保字节流文件中包含的信息符合JVM的要求,如果不符合规范要求则抛出异常 因为class文件不全是Java源码文件编译出来。如果不检查合法性会因有害的字节流导致系统崩溃。
  • 具有大量约束和验证规则的规范
  • 验证阶段完成下面4个动作
    • 文件格式验证 --验查索引处是否有正确值,是否符合编码要求
      • 验证字节(检验的对象是二进制字节流)流是否符合class文件的格式规范。通过该检验后字节流才能进入方法区。
      • 主要目的是确保输入的字节流能正确的解析
        • 魔数
        • 版本号
        • 指向常量池的索引值是否都有了正确的常量
      • 后面的三个验证阶段都是基于方法区(第二步中提到),只有文件格式验证是基于二进制字节流
    • 元数据验证 --元数据的数据类型校验
      • 对类的元数据进行语义校验
      • 检查是否有父类,是否继承了不允许被继承的类
    • 字节码验证 --对类的方法进行校验
      • 主要目的是确定程序语义是合法的、符合逻辑的
        • 操作栈中的类型与指令的类型对应
        • 跳转指令不会跳到错误的地方
      • 为节省时间将验证阶段的“推导状态的动作提前”,在编译阶段将方法的本地变量表和操作栈的状态存储进StackMapTable(还记得吗)中,验证时只需检验该属性的合法性即可。
    • 符号引用验证 --保证解析动作能够正常执行
      • 在将符号引用转换成直接引用的时候进行验证
      • 就是对常量池中的符号引用进行匹配性校验,保证都能找到且找到的都是正确的。
      • 验证符号引用的访问权限(是否可以被当前类访问)。
准备阶段 -正式为类变量分配内存区域并设置初始值
  • 是在方法区中为**类变量(被static修饰的变量)**分配区域,并设置初始值
  • 实例变量会随对象实例化时分配在Java堆中
  • 如若字段属性中有ConstantValue存在,则会为static变量设置值。
解析阶段 -将符号引用替换为直接应用的阶段
  • 符号引用
    • 一组符号(可以说是字符串)用来唯一描述所引用过的目标
    • 所引用的目标不一定是已经加载到内存中
  • 直接引用
    • 与虚拟机的内存分布相关
    • 所引用的目标的实际存储地址
      • 指向目标的指针
      • 能间接定位目标的句柄
      • 或者能定位到目标的偏移量
    • 没有规定解析阶段的具体时间,表示在执行(操作符号引用的)指令之前对符号引用进行解析。
    • 对符号引用的解析可能会有多次,除动态调用外,解析结果可缓存。
    • 解析阶段主要对7种符号引用进行解析
      • 类或者接口
        • 将不是数组类型的类的符号引用交给使用它的类的类加载器(即两个类使用同一个类加载器)
        • 数组类型的的每个元素按照上面的方式解析,最后由虚拟机生成数组对象。
        • 解析完成前进行符号引用验证,验证是否具有访问权限。无访问权限抛出异常。
      • 字段解析
        • 首先要求就是对该字段所属类的符号引用已经被解析。
        • 按照从该类、(继承关系的从下往上)接口、父类搜索该字段相同简单名称和描述符的字段
        • 上述都搜索不到时抛出错误
        • 如若该字段同时出现在自己父类或者接口中时,编译器拒绝编译并抛出异常。
      • 类方法解析
        • 首先确保该类方法所属的类已经被解析
        • 按照该类、父类、(递归的从下到上的搜索)接口
        • 找到返回直接引用,没找到抛出异常。
      • 接口方法解析
        • 同样要确保所属类或接口的符号引用被解析
        • 判断该引用是否为接口,如若不是则抛出异常
        • 在其父接口中递归查找
      • 方法类型
      • 方法句柄
      • 调用点限定符
初始化阶段 -真正执行类中定义的Java程序代码
  • 按照程序员意愿去初始化类变量(准备阶段时设置初始值并不是程序员赋予的值)和其他资源。
  • 可以说初始化阶段就是执行类构造器方法(clinit)的过程
  • clinit方法的形成:
    • 编译器自动收集类中的类变量的赋值动作和静态语句块中的语句合并而成,按照出现顺序。

以上就是将class文件加载进虚拟机的过程也可以说时将class字节流从固态存储地移动到动态内存来给虚拟机使用的过程。在这个过程中可以看到虚拟机是如何将二进制字节流转移到内存中去的,经历了哪些必要的处理才最后存储进我们第二步的存储区中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值