深入理解 Java 虚拟机(四)类文件结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011330638/article/details/82695983

无关性

计算机虽然只能识别 0 和 1,但将代码编译为本地机器码已不再是唯一的选择,越来越多的程序语言选择离了与操作系统无关和机器指令集无关的、平台中立的格式作为代码编译后的存储格式,比如 Java 程序编译后的结果就是字节码,可运行于 Java 虚拟机中。

Java 在诞生之初的宣传口号是“一次编译,到处运行”(Write Once, Run Anywhere),平台无关性的基础就是虚拟机和字节码存储格式。

与此同时,语言无关性也越来越受到重视——即 Java 虚拟机不与 Java 语言绑定,它只与 Class 文件相关联,只要能把 Java、Groovy 等代码编译为 Class 文件即可。

Class 类文件的结构

在关于 Class 文件结构的讲解中,将以《Java 虚拟机规范(第 2 版)》(1999 年发布,对应 JDK 1.4)中的定义为主线,这部分内容虽然古老,但它所包含的指令、属性是 Class 文件最重要和最基础的。

Class 文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,当遇到需要占用一个字节以上的空间的数据项时,则按照 Big-Endian 的方式分割为多个字节进行存储。

Class 文件采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以 u1、u2、u4、u8 分别代码 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用于描述数字、索引引用、数量值或按照 UTF-8 编码构成的字符串值。表是由多个无符号数或其他表作为数据项构成的复合数据类型,通常以 “_info” 结尾,整个 Class 文件本质上就是一张表。

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

魔数与 Class 文件的版本

magic 占 4 个字节,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件,它的值很有浪费气息,为 “CAFEBABA”(咖啡宝贝?)。

紧接着 4 个字节存储的是 Class 文件的版本号:minor_version、major_version。Java 的版本号是从 45 开始的,每一个主版本号向上加 1,高版本 JDK 能向下兼容以前的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。比如 JDK 1.1 的版本号为 45.0 ~ 45.65535,JDK 1.2 则能支持 45.0 ~ 46.65535 的版本号。

下面的内容都将以这段代码为基础进行讲解:

package org.fenixsoft.clazz;

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }

}

使用 WinHex 打开:

这里写图片描述

下面以 “图 1” 代表该图。

可以看到,minor version 的值为 0x0000,major version 的值为 0x0034,即十进制的 52,说明这个是可以被 JDK 1.8 以上版本的虚拟机执行的 Class 文件。

2.2 常量池

再之后是常量池入口,常量池可以理解为 Class 文件的资源仓库,是 Class 文件结构中与其它项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一。

前两个字节是 constant_pool_count,观察上图发现,constant_pool_count 值为 0x0013,即十进制的 19,根据类文件结构,可知常量池中有 18 个常量,为什么是 constant_pool_count-1 呢?这是因为第 0 项常量是 Java 虚拟机规范设计者特意空出来的,用于表达“不引用任何一个常量池项目”。

常量池主要存放两大类常量:字面量和符号引用。字面值指文本字符串、声明为 final 的常量等;符号引用则包括以下三类常量:
1) 类和接口的全限定名(org/fenixsoft/clazz/TestClass)
2) 字段的名称和描述符(Ljava/lang/String)
3) 方法的名称和描述符

常量池每一项常量都是一个表,且都有如下通用格式:

cp_info {
    u1 tag;
    u1 info[];
}

其中第一个 tag 代表常量的类型:

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

再看图 1,第一项常量的 tag 为 0x0A,即十进制的 10,因此该常量类型 CONSTANT_Methodref,结构如下:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_index 是一个索引,值为 0x0004,即指向第 4 项常量,同理,name_and_type_index 值为 0x000F,指向第 15 项常量。

接下来看第二项常量,类型为 0x09,对比上表,可知该常量类型为 CONSTANT_Fieldref,它和 CONSTANT_Methodref 拥有一样的结构:

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

查看图 1 可知 class_index 值为 0x0003,指向第三项常量,name_and_type_index 值为 0x0010,指向第 16 项常量。

接着看第三项常量,也是第二项常量指向的 class,类型为 0x07,即 CONSTANT_Class:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

值为 0x0011,指向第 17 项常量。

第四项常量类型依然为 0x07,即 CONSTANT_Class,也就是第一项常量指向的 class。

以此类推,即可分析出后续的常量及其含义,这里可以借助 javap 直接得到分析结果:

F:\> javap -verbose TestClass.class
Classfile /TestClass.class
  Last modified 2018-8-6; size 295 bytes
  MD5 checksum 81f2ab948a7a3068839b61a8f91f634b
  Compiled from "TestClass.java"
public class org.fenixsoft.clazz.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // org/fenixsoft/clazz/TestClass.m:I
   #3 = Class              #17            // org/fenixsoft/clazz/TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               org/fenixsoft/clazz/TestClass
  #18 = Utf8               java/lang/Object
{
  public org.fenixsoft.clazz.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}
SourceFile: "TestClass.java"

可以看到,对于类、方法、字段,它们都有一个指向 CONSTANT_Utf8_info 的索引,即都需要引用 CONSTANT_Utf8_info 型常量来描述名称,因此 CONSTANT_Utf8_info 型常量的最大长度就是 Java 中方法、字段名的最大长度,而 CONSTANT_Utf8_info 格式如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

CONSTANT_Utf8_info 的长度为 length,是一个 u2 类型的无符号数,所以,Java 中的方法、字段名的最大长度不能超过 64KB。

下面以 “class_info” 代表该分析结果。

访问标志

常量池之后是 access_flags,即访问标志,根据 class_info,可知 TestClass 的访问标志有:ACC_PUBLIC、ACC_SUPER。各个访问标志代表的含义是:

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 是否为 public
ACC_FINAL 0x0010 是否为 final
ACC_SUPER 0x0020 是否允许使用 invokespacial 字节码指令的新语意,JDK 1.2 之后改标志都必须位真
ACC_INTERFACE 0x0200 是否为 interface
ACC_ABSTRACT 0x0400 是否为 abstract
ACC_ABSTRACT 0x1000 标识这个类不是由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

类索引、父类索引和接口索引集合

所有的 Java 类都有父类,因此除了 Object 类之外,所有 Java 类的父类索引都不为 0。

根据 class_info 可知 TestClass 的类索引、父类索引如下:

  #3 = Class  #17// org/fenixsoft/clazz/TestClass
  #4 = Class  #18// java/lang/Object
  ...
  #17 = Utf8   org/fenixsoft/clazz/TestClass
  #18 = Utf8   java/lang/Object

字段表集合

字段(field_info)用于描述接口或类中声明的变量,包括类中声明的成员变量以及静态变量,不包括方法内部声明的局部变量。字段包含的信息有:public/private、static、final、volatile、transient(是否被序列化) 等修饰符和字段数据类型、字段名称等信息。各个修饰符都是布尔值,而字段的数据类型、名称,则引用常量池中的常量来描述。格式如下:

field_info {
   u2 access_flags;
   u2 name_index; // 指向常量池中 Constant_Utf8_info 常量
   u2 descriptor_index;
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

方法表集合

方法表和字段表的格式一样:

method_info {
   u2 access_flags;
   u2 name_index;
   u2 descriptor_index;
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

和字段表不同的是,access_flags 没有 volatile 和 transient 标志,但增加了 synchronized、native、strictfp、abstract 等标志,具体可自行查看 Java VM 规范文档。

而方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中一个名为 “Code” 的属性里面。

属性表集合

Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景转悠的信息。

与 Class 文件中其它的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。

虚拟机规范预定义的属性有:

1) 对于 Java 虚拟机正确解释类文件至关重要的 5 个属性:
• ConstantValue
• Code
• StackMapTable
• Exceptions
• BootstrapMethods

2) 对于 Java SE 平台的类库正确解释类文件至关重要的 12 个属性:
• InnerClasses
• EnclosingMethod
• Synthetic
• Signature
• RuntimeVisibleAnnotations
• RuntimeInvisibleAnnotations
• RuntimeVisibleParameterAnnotations
• RuntimeInvisibleParameterAnnotations
• RuntimeVisibleTypeAnnotations
• RuntimeInvisibleTypeAnnotations
• AnnotationDefault
• MethodParameters

3) 对于解释类文件并不重要,但作为工具很有用的 6 个属性
• SourceFile
• SourceDebugExtension
• LineNumberTable
• LocalVariableTable
• LocalVariableTypeTable
• Deprecated

各个属性的通用格式如下:

attribute_info {
    u2 attribute_name_index; // 指向常量池中的 Constant_Utf8_info 常量
    u4 attribute_length;
    u1 info[attribute_length];
}

Code 属性

Code 属性表定义如下:

Code_attribute {
       u2 attribute_name_index;
       u4 attribute_length;
       u2 max_stack;
       u2 max_locals;
       u4 code_length;
       u1 code[code_length];
       u2 exception_table_length;
       { u2 start_pc;
         u2 end_pc;
         u2 handler_pc;
         u2 catch_type;
       } exception_table[exception_table_length];
       u2 attributes_count;
       attribute_info attributes[attributes_count];
}

max_stack 代表操作数栈深度的最大值。

max_locals 代表局部变量(包括方法参数、显示异常处理器的参数)所需的存储空间,单位是 Slot,byte、char、int、short、boolean 等长度不超过 32 位的数据类型占用 1 个 Slot,double、long 章用 2 个 Slot。Slot 可以重用,比如代码执行超出了一个局部变量的作用域,这个局部变量所占的 Slot 就可以被其它局部变量使用,因此 max_locals 的值小于等于 Slot 的和。

code_length 代表字节码长度,code 是用于存储字节码指令的一系列字节流,每个字节码指令都是一个 u1 类型的单字节。值得注意的是,虽然 code 类型为 u4,但虚拟机规范明确限制了一个方法不允许超过 65536 条字节码指令,如果超过这个限制,javac 编译器会拒绝编译。一般不太可能超过限制,但在某些特殊情况下,比如编译一个很复杂的 jsp 文件时,因为某些 jsp 编译器可能会把 jsp 内容和页面输出的信息归并到一个方法中,而导致方法超长编译失败。

Code 属性是 Class 文件中最重要的一个属性,如果把 Java 程序分为代码和元数据,那么 Code Code 属于就是用于描述代码的,所有其它数据项目都用于描述元数据。

为了方便分析,这里把 TestClass.class 使用 javap 工具分析后的部分结果再贴一遍:

{
  public org.fenixsoft.clazz.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}

可以注意到,TestClass 的 和 inc() 方法虽然都没有参数,但 args_size 都为 1,这是因为 java 编译器编译的时候会把 “this” 作为普通参数自动传入到方法中,因此,局部变量表会预留第一个 Slot 位来存放对象实例的引用,方法参数值从 1 开始计算。但如果是 static 方法,args 就不是 1 而是 0 了。

根据 Code_attribute,code 之后是 exception_table:

u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];

这些字段的含义是:当字节码在第 start_pc 行到 end_pc 行之间出现了类型为 catch_type 或者其子类的异常,则转到 handler_pc 行继续执行。

比如下面这段代码:

public int inc() {
   int x;
   try {
          x = 1;
          return x;
   } catch (Exception e) {
          x = 2;
          return x;
   } finally {
          x = 3;
   }
}

编译后字节码如下:

public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=1
         0: iconst_1 // trye 块中的 1
         1: istore_1
         2: iload_1 // 保存 x 到 returnValue 中,此时 x=1
         3: istore_2
         4: iconst_3 // finally 块中的 3
         5: istore_1
         6: iload_2 // 将 returnValue 中的值放到站定,准备给 ireturn 返回
         7: ireturn
         8: astore_2 // 给 catch 中定义的 Exception e 复制,存储在 Slot 2 中
         9: iconst_2 // catch 块中的 x=2
        10: istore_1
        11: iload_1 // 保存 x 到 returnValue 中,此时 x=2
        12: istore_3
        13: iconst_3 // finally 块中的 3
        14: istore_1
        15: iload_3 // 将 returnValue 中的值放到站定,准备给 ireturn 返回
        16: ireturn
        17: astore        4 // 如果出现了不属于 java.land.Exception 及其子类的异常才会走到这里
        19: iconst_3  // finally 块中的 x=3
        20: istore_1
        21: aload         4 // 将异常放置到栈顶,并输出
        23: athrow
      Exception table:
         from    to  target type
             0     4     8   Class java/lang/Exception
             0     4    17   any
             8    13    17   any
            17    19    17   any

分析可知,代码的执行结果是:如果没有出现异常,则返回 1;出现了 Exception 异常,则返回 2;出现了 Exception 以外的异常,方法非正常退出,没有返回值。

Exceptions 属性

Exceptions 属性的作用是列举出方法中可能抛出的受检查异常,也就是方法描述时在 throws 关键字后面列举的异常。

LineNumberTable 属性

LineNumberTable 用于描述 Java 源码与字节码行号之间的对应关系。

LineNumberTable 不是运行时必需的属性,如果选择不生成该属性,则在程序抛出异常时,堆栈中将无法显示出错的行号,且无法设置断点调试程序。

LocalVariableTable 属性

用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系。

LocalVariableTable 同样不是运行时必须的属性,如果选择不生成该属性,最大的影响是当别人引用这个方法时,所有的参数名称都将丢失,IDE 会使用 arg0、arg1 之类的名称代替原有的参数名,且在调试期间无法根据参数名称获取参数值。

在 JDK 1.5 引入泛型之后,新增了一个属性为 LocalVariableTypeTable,因为 Java 中的泛型是伪泛型,参数化类型会被擦除掉,描述符无法准确地描述泛型类型,因此出现了 LocalVariableTypeTable。

SourceFile 属性

SourceFile 属性用于记录生成这个 Class 文件的源码文件的名称,类名和文件名可能不一致,比如内部类。

这个属性也是可选的,如果选择不生成,当抛出异常时,堆栈中不会显示出错代码所属的文件名。

ConstantValue 属性

ConstantValue 用于通知虚拟机自动为静态变量赋值。

对于非 static 变量的赋值是在构造方法 < init > 中进行的;对于 static 变量,则可以在类构造器 < clinit > 方法中或者使用 ConstantValue 属性。目前 Sun Javac 编译器的选择是:如果同时使用 final 和 static 修饰,并且是基本数据类型或者 String 类型,则生成 ConstantValue 属性进行初始化;否则在 < clinit > 方法中进行初始化。

InnerClass 属性

InnerClass 属性用于记录内部类与宿主类之间的关联。

Deprecated 及 Synthetic 属性

Deprecated 属性用于标识某个类、字段或方法已被程序作者定为不再推荐使用。

Synthetic 属性代表此字段活着方法不是 Java 源码直接产生的,而是编译器自行添加的。

StackMapTable 属性

StackMapTable 是 JDK 1.6 新增的,这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。

一个方法的 Code 属性最多只能有一个 StackMapTable 属性,如果方法中的 Code 属性没有附带 StackMapTable 属性,则意味着它带有一个隐式的 StackMapTable 属性。

Signature 属性

Signature 是 JDK 1.5 之后新增的,可以出现在类、属性和方法表结构的属性表中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。

之所以要专门使用一个属性去记录泛型类型,是因为 Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code 属性)中,泛型信息编译之后都会被擦除掉。使用擦除法的好处是实现简单、节省内存空间,坏处是无法像 C++/C# 那样将泛型与用户定义的普通类型同等对待,例如运行期就无法获取泛型信息。Signature 就是为了解决这个缺陷而设的,现在 Java 的反射 API 能够获取泛型类型,最终的数据来源就是这个属性。

BootstrapMethod 属性

BootstrapMethod 是 JDK 1.7 新增的,位于类文件的属性表中,如果某个类文件结构的常量池中曾经出现过 CONSTANT_InvokeDynamic_info 类型的常量,则这个类文件的属性表中必须存在一个明确的 BootstrapMethod 属性,另外,即使出现过多个 CONSTANT_InvokeDynamic_info 常量,最多也只会有一个 BootstrapMethod。

字节码指令

Java 虚拟机的指令由一个字节长度的、代表着某中特定操作含义的数字(Opcode,称为操作码)以及跟随其后的零个或多个代表此操作所需的参数(Operands,称为操作数)组成。由于 Java 虚拟机采用面向操作数栈而不是寄存器的架构,因此大多数指令都不包含操作数,只有一个操作码。

字节码指令是一种具有鲜明特点,优劣势都很突出的指令集架构。由于限制了 Java 虚拟机操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过 256 条;而对于那些超过一个字节的数据,虚拟机需要在运行时重建出具体数据的结构,这在某种程度上会导致解释执行字节码时会损耗一些性能。但优势也很明显,放弃了操作数对齐,意味着可以省略很多填充和中间符号。这种追求小数据量、高效率传输的设计是 Java 诞生之初面向网络、智能家电的技术背景所决定的,并一直沿用至今。

如果不考虑异常处理,那么 Java 虚拟机的解释器可以使用下面这个伪代码作为最基本的执行模型来理解:

do {
    自动计算 PC 寄存器的值加 1
    根据 PC 寄存器的指示位置,从字节码流中取出操作码
    if (字节码存在操作数) 从字节码流中取出操作数
    执行操作码所定义的操作
} while(字节码流长度 > 0)

字节码与数据类型

大多数的指令都包含了其操作所对应的数据类型,比如 iload、lload、fload、dload、aload 等,分别对应 int、long、float、double、reference。同时,大部分的指令都不支持 byte、char 和 short,甚至没有任何指令支持 boolean,编译器会在编译期或运行期将 byte 和 short 数据类型带符号扩展为 int 类型数据,将 boolean 和 char 零为扩展(无符号)为 int 类型数据。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量和操作数栈之间来回传输,包括:

1) 将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_

2) 将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_

3) 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_

4) 扩充局部变量表的访问索引的指令:wide

iload_指 iload_0、iload_1、iload_2 等指令

运算指令

运算指令用于对两个操作数栈上的值进行某种运算,并把结果重新存入到操作数栈顶。包括:

1) 加法指令:iadd、ladd、fadd、dadd

2) 减法指令:isub、lsub、fsub、dsub

3) 按位或指令:ior、lor

4) ……

类型转换指令

Java 虚拟机直接支持以下几种宽化类型转换:
1) int 类型到 long、float、double
2) long 类型到 float、double
3) flaot 类型到 double

相反,如果是窄化类型转换,必须显式地使用转换指令:i2b、i2c、i2s、l2i……等等。

将一个 float 转换为整数类型 T(int 或 long) 时,遵循以下原则:
1) 如果 float 值为 NaN,则 T 为 0
2) 如果 float 不是无穷大,则使用 IEEE 754 的向零舍入模式取整,获得整数 v,如果 v 在 T 的表示范围之内,则转换结果为 v
3) 否则,将根据 v 的符号,转换为 T 所能表示的最大或最小正数

对象创建和访问指令

类实例和数组都是对象,创建和访问指令包括:

1) new

2) newarray、anewarray 等

3) getfiled、putfield 等

4) ……

操作数栈管理指令

包括:

1) pop、pop2

2) dup、dup2 等

3) swap

控制转移指令

包括:

1) ifeq、iflt、ifle 等

2) tableswitch、lookupswitch

3) goto、goto_w 等

方法调用和返回指令

方法调用将在后面的章节讲解,下面列举几条:

1) invokevirtual,多态

2) invokeinterface,接口方法

3) invokespecial,需要特殊处理的实例方法,比如实例初始化方法、私有方法和父类方法

4) invokestatic,静态方法

5) invokedynamic

方法返回指令包括 ireturn、lretun 等

异常处理指令

显式抛出异常的操作都由 athrow 来完成,而其他许多运行时异常由虚拟机自动抛出,另外,异常的处理是通过异常表而不是字节码指令完成的

同步指令

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程来支持的。

方法级的同步是隐式的,无需字节码指令来控制,虚拟机可根据 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。

同步一段指令序列集通常是由 Java 中的 synchronized 语句块表示的,Java 虚拟机的指令集有 moniterenter 和 moniterexit 两条指令来支持 synchronized 关键字的语义。

编译器必须确保无论方法通过什么方式来完成,每个 moniterenter 指令都必须有一条对应的 moniterexit 指令,无论这个方法是正常结束还是异常结束。

猜你喜欢

转载自blog.csdn.net/u011330638/article/details/82695983