7 字节码加载机制

本章学些目标

  1. jvm如何加载字节码文件?
  2. 字节码文件的内容如何被jvm读取?

特别备注:

  1. 以下内容中的【类】:包含java类和java接口

1 类的生命周期

一、生命周期定义
类被加载到虚拟机内存,到释放出内存的过程

二、生命周期的七个阶段

加载、验证、准备、解析、初始化这五个阶段所执行的具体动作,称为类/接口加载。

2 类加载过程概述

虚拟机把描述类的数据从字节码加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类/接口

如图所示:

注意:Java编译器先把类/接口编译成字节码文件,然后在运行期间,读取字节码文件进行加载。

3 加载时机

3.1 类加载时机

以下六种情况,必须进行类的加载(包括加载、验证、准备、解析、初始化阶段)

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,包括:
    1. 使用new关键字实例化对象
    2. 读取或设置类的静态字段
    3. 调用类的静态方法
  2. 使用java.lang.reflect包的方法对类进行反射调用
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(含main()方法的类),虚拟机会先初始化主类
  5. 如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,则需要先触发其初始化。
  6. 如果接口定义了默认方法(default关键字),如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化【todo:如何从内存层面理解?】

3.2 接口加载时机

针对接口需要做一些特殊说明:

  1. 跟类的初始化过程基本一致。
  2. 接口跟类的初始化差异:
    1. 子类初始化时一定先初始化父类
    2. 接口在初始化时,并不要求其父接口已经初始化,只有在真正使用到父接口的时(如引用接口中定义的常量)才会初始化

4 类加载的过程

类加载的过程包括:加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。

4.1 加载

虚拟机规范:加载过程需要完成这三件事:

  1. 通过类的全名来获取此类的二进制字节流(由类加载器完成)
  2. 将字节流所代表的静态存储结构转化方法区的运行时数据结构
  3. 在内存中生成代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载完成,内存变化情况:(通过验证阶段后,才会真正写入方法区)

4.2 验证

  1. 确保Class文件的字节流信息符合Java虚拟机规范,保证安全。
  2. 优化方案:如果全部代码都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证过程4个阶段:

只有通过了【文件格式验证】阶段之后,这段字节流才被允许进入方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不再直接读取字节流

4.2.1 文件格式验证

目标:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

    1.  是否以魔数0xCAFEBABE开头
    2.  主、次版本号是否在当前虚拟机接受范围之内
    3.  常量池的常量中是否有不被支持的常量类型
    4.  指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    5.  CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
    6.  Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
    7.  等等

4.2.2 元数据验证

目标:对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息

    1.  这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
    2.  这个类的父类是否继承了不允许被继承的类(被final修饰的类)
    3.  如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    4.  类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方 法重载,例如方法参数都一致,但返回值类型却不同等)

4.2.3 字节码验证

目标:通过数据流分析和控制流分析,确定程序语义是合法的。第二阶段完成元数据校验,第三阶段对类的方法体(Code属性)进行校验分析

    1.  保证操作数栈的数据类型与指令代码序列能配合工作,例如不会出现“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
    2.  保证任何跳转指令都不会跳转到方法体以外的字节码指令上
    3.  保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,或者其它类型,则是非法的。

4.2.4 符号引用验证

目标:对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。(当前类依赖的信息(其它类)能否得到满足

    1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
    2. 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
    3.  符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当 前类访问
    
    无法通过符号引用验证,虚拟机抛出java.lang.IncompatibleClassChangeError的子类异常,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

4.3 准备

目标:

  1. 类变量分配内存并设置初始值(零值)
  2. 如果类变量的存在ConstantValue属性(final修饰),那在准备阶段就会赋实际的值

关于初始值的说明:

public static int value = 123;
//那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行

4.4 解析

目标:

  1. Java虚拟机将常量池内的符号引用替换为直接引用

两者对比:

内存目标 形式
符号引用 与JVM的内存布局无关,引用的目标并不一定是已经加载到JVM内存中的内容 任何形式字面量
直接引用 和JVM实现的内存布局直接相关 指针、相对偏移量、能间接定位到目标的句柄

4.4.1 类或接口的解析

假设A存在对B类的引用,解析过程如下:

4.4.2 字段解析

目标:对字段表的类型索引进行解析,即解析字段所属的类或接口

假设类A中,存在字段P,P的类型是B,P解析过程:

4.4.3 方法解析

目标:对方法表的类型索引进行解析,即解析方法所属的类
过程:跟字段解析过程类似。
注意:如果方法所属的目标类型B是一个接口,那么报错中断

4.4.4 接口方法解析

目标:对方法表的类型索引进行解析,即解析方法所属的接口
过程:跟方法解析过程类型,不同之处如下:

  1. 如果方法所属的类型是类,那么解析报错中断。
  2. 因为接口允许多重继承,如果目标类型C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找。

4.5 初始化

直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程

目标:

  1. 执行类构造器()方法的过程。
  2. 实际效果:完成类变量的实际值初始化

4.5.1 关于类构造器clinit()方法

  1. 它不是程序员在代码中直接编写的方法,它是Javac编译器的自动生成物。
  2. 由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
  3. 准备阶段完成类变量的零值初始化,在初始化阶段由clinit() 完成类变量的实际值的初始化
posted @ 2022-11-13 20:52  拿了桔子跑-范德依彪  阅读(74)  评论(0编辑  收藏  举报