美文网首页一些收藏JVM
JVM学习14·Class 文件结构及深入字节码指令

JVM学习14·Class 文件结构及深入字节码指令

作者: 那钱有着落吗 | 来源:发表于2022-01-28 11:17 被阅读0次

    本节内容较为深涩难懂,所以仅做了解即可,实际工作中也很少用到,只是说了解之后会让我们更加了解jvm,仅此而已

    1.JVM的无关性

    与平台无关性是建立在操作系统上,虚拟机厂商提供了许多可以运行在各种不同平台的虚拟机,它们都可以载入和执行字节码,从而实现程序的“一次 编写,到处运行”
    https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石,也是语言无关性的基础。Java 虚拟机不 和包括 Java 在内的任何语言绑定,它只与“Class 文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他 辅助信息。

    2.Class文件格式


    Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义, 长度是多少,先后顺序如何,都不允许改变。
    按顺序包括:

    2.1魔数与 Class 文件的版本

    每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。使用魔数而不是扩展
    名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛
    采用过同时又不会引起混淆即可。

    紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号(MinorVersion),第 7 和第 8 个字节是主版本号(Major Version)。
    Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版 本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。


    代表 JDK1.8(16 进制的 34,换成 10 进制就是 52)

    2.2常量池

    常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
    与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的


    常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。 字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:
    类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符


    我们就可以使用更加直观的工具 jclasslib,来查看字节码中的具体内容了

    2.3访问标志

    用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被
    声明为 final 等 类索引、父类索引与接口索引集合
    这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承, 所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引 集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序 从左到右排列在接口索引集合中

    24字段表集合

    描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。 而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问 性,会自动添加指向外部类实例的字段。

    2.5方法表集合

    描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。 与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”

    2.6属性表集合

    存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。

    2.7字节码指令(不重要)

    字节码指令属于方法表中的内容:


    Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操 作数,Operands)而构成。
    由于限制了 Java 虚拟机操作码的长度为一个字节(即 0~255),这意味着指令集的操作码总数不可能超过 256 条。 大多数的指令都包含了其操作所对应的数据类型信息。例如:
    iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据。
    大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。大多数对于 boolean、byte、short 和 char 类型数据的操作,

    实际上都是使用相应的 int 类型作为运算类型
    阅读字节码作为了解 Java 虚拟机的基础技能,有需要的话可以去掌握常见指令。 字节码助记码解释地址:https://cloud.tencent.com/developer/article/1333540

    2.8加载和存储指令

    用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。 将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。 将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n >。 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。 扩充局部变量表的访问索引的指令:wide。

    2.9运算或算术指令

    用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。 加法指令:iadd、ladd、fadd、dadd。
    减法指令:isub、lsub、fsub、dsub。
    乘法指令:imul、lmul、fmul、dmul 等等

    2.10类型转换指令

    可以将两种不同的数值类型进行相互转换,
    Java 虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换):
    int 类型到 long、float 或者 double 类型。
    long 类型到 float、double 类型。
    float 类型到 double 类型。
    处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。

    2.11创建类实例的指令 new。

    2.12创建数组的指令 newarray、anewarray、multianewarray。

    2.13访问字段指令 getfield、putfield、getstatic、putstatic。

    2.14数组存取相关指令

    把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。 取数组长度的指令:arraylength。

    2.15检查类实例类型的指令 instanceof、checkcast。

    2.16操作数栈管理指令

    如同操作一个普通数据结构中的堆栈那样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:pop、 pop2。
    复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。 将栈最顶端的两个数值互换:swap。

    2.17控制转移指令

    控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控 制转移指令就是在有条件或无条件地修改 PC 寄存器的值。控制转移指令如下。 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。 复合条件分支:tableswitch、lookupswitch。
    无条件分支:goto、goto_w、jsr、jsr_w、ret。

    2.18方法调用指令

    invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。 invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
    invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
    invokestatic 指令用于调用类方法(static 方法)。
    invokedynamic 指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的。
    方法调用指令与数据类型无关。

    2.19方法返回指令

    是根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有
    一条 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。 ##2.20异常处理指令
    在 Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令来实现

    2.21同步指令

    有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义

    2.22字节码指令——异常处理

    每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。 当一个方法执行完,要返回,那么有两种情况,一种是正常,另外一种是异常
    完成出口(返回地址): 正常返回:(调用程序计数器中的地址作为返回) 三步曲:
    恢复上层方法的局部变量表和操作数栈、 把返回值(如果有的话)压入调用者栈帧的操作数栈中、 调整程序计数器的值以指向方法调用指令后面的一条指令、 异常的话:(通过异常处理表<非栈帧中的>来确定)

    3.异常机制



    3.1type 异常类型

    也就是说,只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。 我可以看到,第一条 monitorexit(16)在异常表第一条的范围中,如果异常,能够跳转到第 20 行 第二条 monitorexit(22)在异常表第二条的范围中,如果异常,能够跳转到第 20 行

    3.2Finally

    通常我们在做一些文件读取的时候,都会在 finally 代码块中关闭流,以避免内存的溢出。关于这个场景,我们再分析一下下面这段代码的异常表。



    4.字节码指令——装箱拆箱

    这里有一个概念就是装箱拆箱,比较基础,也经常在基本的面试中遇到,不懂得小伙伴可以好好去学习一下。

    Java 中有 8 种基本类型,但鉴于 Java 面向对象的特点,它们同样有着对应的 8 个包装类型,比如 int 和 Integer,包装类型的值可以为 null(基本类 型没有null值,而数据库的表中普遍存在null值。 所以实体类中所有属性均应采用封装类型),很多时候,它们都能够相互赋值。



    相关文章

      网友评论

        本文标题:JVM学习14·Class 文件结构及深入字节码指令

        本文链接:https://www.haomeiwen.com/subject/cmfahrtx.html