0%

JVM Class文件结构

代码编译的结果从夲地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。将编写的程序编译成二进制本地机器码( Native Code )已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

Class的类文件结构

根据 Java 虚拟机规范的规定, Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,由下表所示的数据项构成。

类型 名称 解释 数量
u4 magic 魔数 1
u2 minor_version 副版本号 1
u2 major_version 主版本号 1
u2 constant_pool_count 常量池总数 1
cp_info constant_pool 常量池数组 constant_pool_count - 1
u2 access_flag 访问标志,如 public 1
u2 this_class 类索引 1
u2 super_class 父类索引 1
u2 interfaces_count 接口总数 1
u2 interfaces 接口数组 interfaces_count
u2 fields_count 字段总数 1
field_info fields 字段数组 fields_count
u2 methods_count 方法总数 1
method_info methods 方法数组 methods_count
u2 attributes_count 属性总数 1
attribute_info attributes 属性数组 attributes_count

魔数与Class文件版本

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

紧接着是 Class 文件的次版本号(Minor Version)和主版本号(Major Version)。

编译器版本 -target参数 十六进制版本号 十进制版本号
JDK 1.8.0 不带(默认为 -target 1.8) 00 00 00 34 52.0
JDK 1.8.0 -target 1.7 00 00 00 33 51.0

下面是 Java 文件以及其编译后的 Class 文件的十六进制显示。

public class Solution {
}
  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 34 00 0D 0A 00 03 00 0A 07    J~:>...4........
00000010: 00 0B 07 00 0C 01 00 06 3C 69 6E 69 74 3E 01 00    ........<init>..
00000020: 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69    .()V...Code...Li
00000030: 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 0A    neNumberTable...
00000040: 53 6F 75 72 63 65 46 69 6C 65 01 00 0D 53 6F 6C    SourceFile...Sol
00000050: 75 74 69 6F 6E 2E 6A 61 76 61 0C 00 04 00 05 01    ution.java......
00000060: 00 08 53 6F 6C 75 74 69 6F 6E 01 00 10 6A 61 76    ..Solution...jav
00000070: 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00    a/lang/Object.!.
00000080: 02 00 03 00 00 00 00 00 01 00 01 00 04 00 05 00    ................
00000090: 01 00 06 00 00 00 1D 00 01 00 01 00 00 00 05 2A    ...............*
000000a0: B7 00 01 B1 00 00 00 01 00 07 00 00 00 06 00 01    7..1............
000000b0: 00 00 00 01 00 01 00 08 00 00 00 02 00 09          ..............

常量池

常量池可以理解为 Class 文件之中的资源仓库。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant pool count)。

容量计数是从 1 而不是从 0 开始的

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

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java 代码在进行 Javac 编译的时候,并不像 C 和 C++ 那样有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。也就是说,在 Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在介绍虚拟机类加载过程时再进行详细讲解。

常量池中每一项常量都是一个表,在 JDK1.7 之前共有 11 种结构各不相同的表结构数据。在 JDK 1.7 中为了更好地支持动态语言调用,又额外增加了 3 种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info 和 CONSTANT_InvokeDynamic_info),会在字节码执行和方法调用时讲解。常量池类型如下表所示。

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Double_info 5 双精度浮点型字面量
CONSTANT_Long_info 6 长整型字面量
CONSTANT_Class_info 7 类与接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段和方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

由于常量池的 14 种类型各自均有自己的结构,下图列出了这些结构的表现列表

访问标志

访问标志(access flags)在常量池定义之后,占用两字节,这个标志用于识别一些类或者接口层次的访问信息具体的标志位以及标志的含义见下表。

标志名称 标志值 含义
ACC_PUBLISH 0x0001 publish类型标识
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语意, invokespecial 指令的语意在 JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2 之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 接口标识
ACC_ABSTRACT 0x0400 对于抽象类或接口,此标志位为空,其他类值为假
ACC_SYNTHENIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 注解类标识
ACC_ENUM 0x4000 枚举类标识

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

类索引(this_class)和父类索引(super_class)都是个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合, Class 文件中由这三项数据来确定这个类的继承关系。这三项数据按以上顺序排列在访问标志之后。

类索引用于确定这个类的全限定名。

父类索引用于确定这个类的父类的全限定名。因为 Java 语言不允许多重继承,所以父类索引只有一个。除了 Object 类以外,所有的 Java 都有父类,父类索引都不为 0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按照 implement 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。需要注意的是,在接口索引集合之前需要有一个 u2 类型的数据为接口计数器(interface count),表示索引表的容量。

方法表集合

方法表的结构包括了访问标志(access flags)、名称索引(name index)、描述符索引(descriptor index)、属性表集合(attributes)。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志。与之相对的, synchronized、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志。对于方法表所有标志位及其取值可参见下表。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 public 方法标识
ACC_PRIVATE 0x0002 private 方法标识
ACC_PROTECTED 0x0004 protected 方法标识
ACC_STATIC 0x0008 static 方法标识
ACC_FINAL 0x0010 final 方法标识
ACC_SYNCHRONIZED 0x0020 synchronized 方法标识
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 native 方法标识
ACC_ABSTRACT 0x0400 abstract 方法标识
ACC_STRICTFP 0x0800 strictfp 方法标识
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

方法里的 Java 代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的“Code”属性里面,属性表作为 Class 文件格式中最具扩展性的一种数据项目。

与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>方法和实例构造器<init>方法。

在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此 Java 语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共在于同一个 Class 文件中的。

属性表集合

与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写人自定义的属性信息:Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析 Class 文件;《Java虛拟机规范(Java SE7)》中预定义了 21 项虚拟机实现应当能识别的属性,具体内容见下表。

对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所用的位数即可。一个符合规则的属性表应该满足下表中所定义的结构。

类别 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

Code属性

Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在 Code 属性,如果方法表有 Code 属性存在,那么它的结构将如下表所示。

类型 名称 数量 解释
u2 attribute_name_index 1 指向 CONSTANT_Utf8_info型常量的索引,常量值固定为 Code,代表该属性的属性名称
u4 attribute_length 1 后续属性表长度(Code属性表长度减去属性名称索引以及属性长度的6个字节)
u2 max_stack 1 操作数栈(Operand Stacks)深度的最大值,JVM根据此值分配栈帧中操作数栈的深度
u2 max_locals 1 局部变量表所需的的存储空间
u4 code_length 1 和code用来存储Java源程序编译后生成的字节码指令,code_length代表字节码长度
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

max_locals

max_locals 代表了局部变量表所需的存储空间。在这里,max_locals 的单位是 Slot,Siot 是虚拟机为局部变量分配内存所使用的最小单位。对于 byte、char、float、int、short、boolean 和 returnAddress 等长度不超过 32 位的数据类型,每个局部变量占用 1 个 Slot,而 double 和 long 这两种 64 位的数据类型则需要两个 Slot 来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是 try-catch 语句中 catch 块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,局部变量表中的 Slot 可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的 Slot 可以被其他局部变量所使用, Javac 编译器会根据变量的作用域来分配 Slot 给各个变量使用,然后计算出 max_locals 的大小。

code_length&code

code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。 code_length 代表字节码长度,code 是用于存储字节码指令的一系列字节流。需要注意的是,每个字节码指令就是一个 u1 类型的单字节。

关于 code_length,有一件值得注意的事情,虽然它是一个 u4 类型的长度值,理论上最大值可以达到2^32-1,但是虚拟机规范中明确限制了一个方法不允许超过 65535 条字节码指令,即它实际只使用了 u2 的长度,如果超过这个限制,Javac 编译器也会拒绝编译。一般来讲,编写 Java 代码时只要不是刻意去编写—个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的 JSP 文件时,某些 JSP 编译器会把 JSP 内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。

code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的借息分为代码(Code,方法体里面的 Java 代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个 Class 文件中,Code 属性用于描述代码,所有的其他数据项目都用于描述元数据。

public class Solution {
    private int m;

    public int inc() {
        return m + 1;
    }
}
//javac -g:vars Solution.java
//javap -verbose Solution
Classfile /D:/Workspace/cpf_tsps/module-system/src/main/test/Solution.class
  Last modified 2021-2-19; size 271 bytes
  MD5 checksum a7f2540ecef9a7617022fbf890f7018a
public class Solution
  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         // Solution.m:I
   #3 = Class              #17            // Solution
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>         //实例构造器标识
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LSolution;
  #13 = Utf8               inc
  #14 = Utf8               ()I
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               Solution
  #18 = Utf8               java/lang/Object
{
  public Solution();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSolution;

  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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   LSolution;
}

无参方法的 args_size=1,方法未使用局部变量 locals=1 的原因:

这与通过 this 关键字访问此方法所属对象的实现方式有关,通过 Javac 编译器编译的时候把对 this 关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传人此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值从 1 开始计算。这个处理只对实例方法有效。

如果方法为 static 时, args_size=0 locals=0

exception_table_length&exception_table

异常表的格式如下表所示,它包含4个字段,这些字段的含义为:如果当字节码在第 start_pc 行到第 end_pc 行之间(不含第 end_pc 行)出现了类型为 catch_type 或者其子类的异常(catch type 为指向一个 CONSTANT_Class_info 型常量的索引),则转到第 handler_pc 行继续处理。当 catch_type 的值为 0 时,代表任意异常情况都需要转向到 handler_pc 处进行处理。

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

编译器会使用 exception_table 而不是简单的跳转实现try-catch-finally处理机制

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	//try块中的x=1
       1: istore_1	
       2: iload_1		//保存x到returnVa1ue中,此时x=1
       3: istore_2
       4: iconst_3	//finally块中的x=3
       5: istore_1
       6: iload_2     //将returnVa1ue中的值放到栈顶,准备给ireturn返回
       7: ireturn
       8: astore_2	//给catch中定义的Exception e赋值,存储在Slot2中
       9: iconst_2	//catch块中的x=2
      10: istore_1
      11: iload_1
      12: istore_3
      13: iconst_3	//finally块中的x=3
      14: istore_1
      15: iload_3		//将returnVa1ue中的值放到栈顶,准备给ireturn返回
      16: ireturn
      17: astore        4 //如果出现了不属于java.1ang.Exception及其子类的异常才会走到这里
      19: iconst_3	//fina1y块中的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
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          2       6     1     x   I
          9       8     2     e   Ljava/lang/Exception;
         11       6     1     x   I
          0      24     0  this   LSolution;
         21       3     1     x   I
    StackMapTable: number_of_entries = 2
      frame_type = 72 /* same_locals_1_stack_item */
        stack = [ class java/lang/Exception ]
      frame_type = 72 /* same_locals_1_stack_item */
        stack = [ class java/lang/Throwable ]

编译器为这段 Java 源码生成了 3 条异常表记录,对应 3 条可能出现的代码执行路径。从 Java 代码的语义上讲,这 3 条执行路径分别为:

  • 如果 try 语句块中出现属于 Exception 或其子类的异常,则转到 catch 语句块处理。
  • 如果 try 语句块中出现不属于 Exception 或其子类的异常,则转到 finally 语句块处理。
  • 如果 catch 语句块中出现任何异常,则转到 finally 语句块处理。
  • 如果 finally 语句块中出现异常,则抛出异常。

Exceptions属性

Exceptions 属性与 Code 属性平级。 Exceptions 属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在 throws 关键字后面列举的异常。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

Exceptions 属性中的 number_of_exceptions 项表示方法可能抛出 number_of_exceptions 种受查异常,exception_index_table 是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该受查异常的类型。

LineNumberTable属性

LineNumberTable 属性用于描述 Java 源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用-g.none-g:lines选项来取消或要求生成这项信息。如果选择不生成 LineNumberTable 属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

LineNumberTable 属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

line_number_table 是一个数量为 line_number_table_length、类型为 line_number_info 的集合,line_number_info 表包括了 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 Java 源码行号。

LocalVariableTable属性

LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用-g.none-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE 将会使用诸如 arg0、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

LocalVariableTable属性结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

local_variable_info属性结构见下表。

类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

start_pc 和 length 属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index 和 descriptor_index 都是指向常量池中 CONSTANT_Utf8_info 型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

index 是这个局部变量在栈帧局部变量表中 Slot 的位置。当这个变量数据类型是 64 位类型时(double 和 long),它占用的 Slot 为 index 和 index+1 两个。

顺便提一下,在 JDK1.5 引入泛型之后, LocalVariableTable 属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。

SourceFile属性

SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。这个属性也是可选的,可以在 Javac 中分别使用-g.none-g:vars选项来取消或要求生成这项信息。在 Java 中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是个定长的属性,其结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

sourcefile_index 数据项是指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源码文件的文件名。

ConstantValue属性

ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键字修饰的变量(类变量)可以使用这项属性。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用 ConstantValue 属性。目前 Sun javac 编译器的选择是:如果同时使用 final 和 static 来修饰一个变量(按照习惯,这里称“常量”更姑切),并且这个变量的数据类型是基本类型或者 java.lang.String 的话,就生成 ConstantValue 属性来进行初始化,如果这个变量没有被 final 修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

虽然有 final 关键字才更符合 ConstantValue 的语义,但虚拟机规范中并没有强制要求字段必须设置了 ACC_FINAL 标志,只要求了有 ConstantValue 属性的字段必须设置 ACC_STATIC 标志而已,对 final 关键字的要求是 Javac 编译器自己加入的限制。而对 ConstantValue 的属性值只能限于基本类型和 String,不过笔者不认为这是什么限制,因为此属性的属性值只是一个常量池的索引号,由于 Class 文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算 ConstantValue 属性想支持别的类型也无能为力。

ConstantValue属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

从数据结构中可以看出, ConstantValue 属性是一个定长属性,它的 attribute_length 数据项值必须固定为 2。 constantvalue_index 数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是 CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info 常量中的一种。

InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClasses 属性。该属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_class_info inner_classes number_of_classes

数据项 number_of_classes 代表需要记录多少个内部类信息,每一个内部类的信息都由一个 inner_classes_info 表进行描述。 inner_classes_info 表的结构见下表。

类型 名称 数量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1

inner_class_info_index 和 outer_class_info_index 都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_index 是指向常量池中 CONSTANT_Utf8_info 型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为 0。

inner_class_access_flags 是内部类的访问标志,类似于类的 access_flags,它的取值范围见下表。

标志名称 标志值 含义
ACC_PUBLISH 0x0001 publish类型标识
ACC_PRIVATE 0x0002 private类型标识
ACC_PROTECTED 0x0004 protected类型标识
ACC_STATIC 0x0008 static类型标识
ACC_FINAL 0x0010 final类型标识
ACC_INTERFACE 0x0200 接口标识
ACC_ABSTRACT 0x0400 对于抽象类或接口,此标志位为空,其他类值为假
ACC_SYNTHENIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 注解类标识
ACC_ENUM 0x4000 枚举类标识

Deprecated及Synthetic属性

Deprecated 和 Synthetic 两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用。它可以通过在代码中使用 @Deprecated 注释进行设置。

Synthetic 属性代表此字段或者方法并不是由 Java 源码直接产生的,而是由编译器自行添加的,在 JDK1.5 之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_ SYNTHETIC 标志位,其中最典型的例子就是 Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置 Synthetic 属性和 ACC_SYNTHETIC 标志位中的一项,唯一的例外是实例构造器<init>方法和类构造器<clinit>方法。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

StackMapTable属性

StackMapTable 属性在 JDK1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性,位于 Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

这个类型检查验证器最初来源于 Sheng Liang(听名字似乎是虚拟机团队中的华裔成员)为 Java ME CLDC 实现的字节码验证器。新的验证器在同样能保证 Class 文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型(Verification Types)直接记录在 Class 文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。

StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检査目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

StackMapTable 属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

《Java 虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于 50.0 的 Class 文件中,如果方法的 Code 属性中没有附带 StackMapTable 属性;那就意味着它带有个隐式的 StackMap 属性这个 StackMap 属性的作用等同于 number_of_entries 值为 0 的 StackMapTable 属性。一个方法的 Code 属性最多只能有一个 StackMapTable 属性,否则将抛出 ClassFormatError 异常。

Signature属性

Signature 属性在 JDK1.5 发布后增加到了 Class 文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中在 JDK1.5 中大幅增强了:Java 语言的语法,在此之后,任何类接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型:是因为 Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改 Javac 编译器,虚拟机内部只做了很少的改动)、非常容易实现 Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。Signature 属性就是为了弥补这个缺陷而增设的,现在 Java 的反射 APl 能够获取泛型类型,最终的数据来源也就是这个属性。Signature属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

其中 signature_index 项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示类签名、方法类型签名或字段类型签名。如果当前的 Signature 属性是类文件的属性,则这个结构表示类签名,如果当前的 Signature 属性是方法表的属性,则这个结构表示方法类型签名,如果当前 Signature 属性是字段表的属性,则这个结构表示字段类型签名。

BootstrapMethods属性

BootstrapMethods 属性在 JDK1.7 发布后增加到了 Class 文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存 invokedynamic 指令引用的引导方法限定符。《Java虚拟机规范(Java SE 7版)》规定,如果某个类文件结构的常量池中曾经出现过 CONSTANT_InvokeDynamic_info 类型的常量,那么这个类文件的属性表中必须存在个明确的 BootstrapMethods 属性,另外,即使 CONSTANT_InvokeDynamic_info 类型的常量在常量池中出现过多次,类文件的属性表中最多也只能有一个 BootstrapMethods 属性。BootstrapMethods 属性与 JSR-292 中的 InvokeDynamic 指令和 java.lang.Invoke 包关系非常密切。

目前的 Javac 暂时无法生成 InvokeDynamic 指令和 BootstrapMethods属性,必须通过些非常规的手段才能使用到它们。BootstrapMethods 属性的结构见下表。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap-method bootstrap_methods num_bootstrap_methods

其中引用到的 bootstrap_method 结构见下表

类型 名称 数量 解释
u2 bootstrap_method_ref 1 必须是一个对常量池的有效索引。
常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构
u2 num_bootstrap_arguments 1 bootstrap_arguments数组成员的数量。
u2 bootstrap_arguments num_bootstrap_arguments

BootstrapMethods 属性中,num_bootstrap_methods 项的值给出了 bootstrap_methods[] 数组中的引导方法限定符的数量。而 bootstrap_methods[] 数组的每个成员包含了一个指向常量池 CONSTANT_MethodHandle 结构的索引值,它代表了一个引导方法,还包含了这个引导方法静态参数的序列(可能为空)。

bootstrap_arguments:bootstrap_arguments 数组的每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构之一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSEANT_MethodHandle_info、CONSTANT_MethodType_info。

字节码指令

JVM 的指令由一个字节长度的操作码以及跟随其后的零至多个操作数组成。由于 JVM 采用面向操作数栈而不是寄存器的架构(这两种架构的区别和影响将在后面探讨),所以大多数的指令都不包含操作数,只有一个操作码。

字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了 Java 虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过 256 条;又由于 Class 文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果要将一个 16 位长度的无符号整数使用两个无符号字节存储起来(将它们命名为 byte1 和 byte2 ),那它们的值应该是这样的:(byte1 << 8) | byte2。这种操作在某种程度上会导致解释执行字节码时损失一些性能。但这样做的优势也非常明显,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。这种追求尽可能小数据量、高传输效率的设计是由 Java 语言设计之初面向网络、智能家电的技术背景所决定的,并一直沿用至今。

字节码的数据类型

在 Java 虚拟机的指令集中,大多数的指令都是以支持的数据类型+操作类型的格式命名的。如 iload 指令代表从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据。这两条指令的操作可能会是由同一段代码来实现的,但它们必须拥有各自独立的操作符。

对于大部分为与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i 代表对 int 类型的数据操作,l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。也有一些指令的助记符中没有明确的指明操作类型的字母,例如 arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,例如无条件跳转指令 goto 则是与数据类型无关的。

由于 Java 虚拟机的操作码长度只有一个字节,所以包含了数据类型的操作码对指令集的设计带来了很大的压力:如果每一种与数据类型相关的指令都支持 Java 虚拟机所有运行时数据类型的话,那恐怕就会超出一个字节所能表示的数量范围了。因此,Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,换句话说,指令集将会故意被设计成非完全独立的(Not Orthogonal,即并非每种数据类型和每一种操作都有对应的指令)。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

下表列举了 Java 虚拟机所支持的字节码指令集,通过使用数据类型列所代表的特殊字符替换 opcode 列的指令模板中的 T,就可以得到一个具体的字节码指令。如果在表中指令模板与数据类型两列共同确定的格为空,则说明虚拟机不支持对这种数据类型执行这项操作。例如 load 指令有操作 int 类型的 iload,但是没有操作 byte 类型的同类指令。

请注意,从下表中看来,大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。编译器会在编译期或运行期会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据。与之类似的,在处理 boolean、byte、short 和 char 类型的数组时,也会转换为使用对应的 int 类型的字节码指令来处理。因此,大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的对 int 类型作为运算类型(Computational Type)。

加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输

局部变量表到操作数栈

类型 解释 示例
<type>load 将一个局部变量加载到操作数栈 iload(= iload_0)lload
<type>load_<n> 代表一组load指令 iload_<n>(= iload_0, iload_1, iload_2, iload_3)

_<n>的详解

形如xxx_<n>以尖括号结尾的代表了一组指令 (例如iload_<n> 代表了(iload_0 iload_1 iload_2 iload_3) 。这一组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式。对于这些特殊形式来说,他们表面上没有操作数,但是操作数隐含在指令里面了。除此之外,语义与原指令并没有任何的不同(例如 iload_0 的语义与操作数为 0 时的 iload 语义完全相同)。

需要注意的是,_<n>的形式不是无限的,对于 load 和 store 系列指令,超过 4 个(也就是下标是 4 的第 5 个),往后都是直接只用原始形式iload 4不再使用_<n>的形式 所以你不会看到 load_4 load_5…. 对于虚拟机执行方法来说,操作数栈是工作区,所以数据的流向是对于操作数栈来说的。load 就是局部变量数据加载到操作数栈,store 就是从操作数栈存储到局部变量表。对于常量只有加载到操作数栈进行使用,没有存储的说法。

操作数栈到局部变量表

类型 解释 示例
<type>store 将一个数值从操作数栈存储到局部变量表 istore(= istore_0)lstore
<type>store_<n> 代表一组store指令 istore_<n>(= istore_0, istore_1, istore_2, istore_3)

常量加载到操作数栈

const 常量的说明如下表

指令码 助记符 说明
0x01 aconst_null 将 null 压入栈顶
0x02 iconst_m1 将 i -1压入栈顶
0x03 iconst_0 将 i 0 压入栈顶
0x04 iconst_1 将 i 1 压入栈顶
0x05 iconst_2 将 i 2 压入栈顶
0x06 iconst_3 将 i 3 压入栈顶
0x07 iconst_4 将 i 4 压入栈顶
0x08 iconst_5 将 i 5 压入栈顶
0x09 lconst_0 将 l 0 压入栈顶
0x0a lconst_1 将 l 1 压入栈顶
0x0b fconst_0 将 f 0 压入栈顶
0x0c fconst_1 将 f 1 压入栈顶
0x0d fconst_2 将 f 2 压入栈顶
0x0e dconst_0 将 d 0 压入栈顶
0x0f dconst_1 将 d 1 压入栈顶

push 常量的说明如下表

指令码 助记符 说明
0x10 bipush 将 b -128~127 压栈
0x11 sipush 将 s -32768~32767 压栈

ldc 常量的说明如下表

ldc 命令负责把数值常量或 String 常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置(编号)的参数,也就是行号。

指令码 助记符 说明
0x12 ldc 将 int、float 或 String 型常量值从常量池中推送至栈顶
0x13 ldc_w 将 int、float 或 String 型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w 将 long 或 double 型常量值从常量池中推送至栈顶(宽索引)

A1:哪些常量才会存入常量池?
static final 标识的数值类型以及不是通过 new 创建的 String 类型。

A2:宽索引是什么?
宽索引是指常量池行号索引的字段长度,ldc 的索引只有 8 位,ldc_w 的索引则有 16 位。对于宽索引,指令格式为ldc_w indexbyte1 indexbyte2会计算(indexbyte1<<8) | indexbyte2来生成一个指向当前常量池的无符号 16 位索引。

扩充局部变量表的访问索引

扩充局部变量表的访问索引使用 wide 指令。访问对象的字段或数组元素的指令也同样会与操作数栈传输数据。宽索引字节码的指令是单字节的。对于局部变量来说,最多容纳 256 个局部变量,wide 指令就是用于扩展局部变量数的。将 8 位的索引扩展为 16 位最多 65536。

运算指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。数据没有直接支持 byte、short、char 和 boolean 类型的算术指令,对于这些数据的运算,都是使用操作 int 类型的指令。

整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为,所有的算术指令包括:

指令类型 指令
加法指令 iadd、ladd、fadd、dadd
减法指令 isub、lsub、fsub、dsub
乘法指令 imul、lmul、fmul、dmul
除法指令 idiv、ldiv、fdiv、ddiv
求余指令 irem、lrem、frem、drem
取反指令 ineg、lneg、fneg、dneg
位移指令 ishl、ishr、iushr、lshl、lshr、lushr
按位或指令 ior、lor
按位与指令 iand、land
按位异或指令 ixor、lxor
局部变量自增指令 iinc
比较指令 dcmpg、dcmpl、fcmpg、fcmpl、lcmp

Java 虚拟机的指令集直接支持了在《Java 语言规范》中描述的各种对整数及浮点数操作的语义。

Java 虚拟机没有明确规定整型数据溢出的情况,但是规定了在处理整型数据时,只有除法指令(idiv 和 ldiv)以及求余指令(irem 和 lrem)出现除数为零时会导致虚拟机抛出异常,如果发生了这种情况,虚拟机将会抛出 ArithmeitcException 异常。

Java 虚拟机在处理浮点数时,必须遵循 IEEE 754 规范中所规定行为限制。也就是说 Java 虚拟机要求完全支持 IEEE 754 中定义的非正规浮点数值(Denormalized Floating-Point Numbers)和逐级下溢(Gradual Underflow)。这些特征将会使得某些数值算法处理起来变得更容易一些。

Java 虚拟机要求在进行浮点数运算时,所有的运算结果都必须舍入到适当的进度,非精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,那将优先选择最低有效位为零的。这种舍入模式也是 IEEE 754 规范中的默认舍入模式,称为向最接近数舍入模式。

在把浮点数转换为整数时,Java 虚拟机使用 IEEE 754 标准中的向零舍入模式,这种模式的舍入结果会导致数字被截断,所有小数部分的有效字节都会被丢弃掉。向零舍入模式将在目标数值类型中选择一个最接近,但是不大于原值的数字来作为最精确的舍入结果。

Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常(这里所讲的是 Java 的异常,请勿与 IEEE 754 规范中的浮点异常互相混淆),当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会时候 NaN 值来表示。所有使用 NaN 值作为操作数的算术操作,结果都会返回 NaN。

在对 long 类型数值进行比较时,虚拟机采用带符号的比较方式,而对浮点数值进行比较时(dcmpg、dcmpl、fcmpg、fcmpl),虚拟机采用 IEEE 754 规范说定义的无信号比较(Nonsignaling Comparisons)方式。

类型转换指令

类型转换指令可以将两种 Java 虚拟机数值类型进行相互转换,这些转换操作一般用于实现用户代码的显式类型转换操作,或者用来处理 Java 虚拟机字节码指令集中指令非完全独立独立的问题。

宽化类型转换

Java 虚拟机直接支持以下数值的宽化类型转换(Widening Numeric Conversions,小范围类型向大范围类型的安全转换)

  • int 类型到 long、float 或者 double 类型
  • long 类型到 float、double 类型
  • float 类型到 double 类型

“直接支持”意味着转换时无需显式的转换指令

窄化类型转换

窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。

在将 int 或 long 类型窄化转换为整数类型 T 的时候,转换过程仅仅是简单的丢弃除最低位 N 个字节以外的内容,N 是类型 T 的数据类型长度,这将可能导致转换结果与输入值有不同的正负号(注:在高位字节符号位被丢弃了)。

在将一个浮点值转窄化转换为整数类型 T(T 限于 int 或 long 类型之一)的时候,将遵循以下转换规则:

如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 ,否则,如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,这时候可能有两种情况:如果 T 是 long/int 类型,并且转换结果在 long/int 类型的表示范围之内,那就转换为 long/int 类型数值 v。否则, 如果转换结果 v 的值太小(包括足够小的负数以及负无穷大的情况),无法使用 T 类型表示的话,那转换结果取 int 或 long 类型所能表示的最小数字。如果转换结果 v 的值太大(包括足够大的正数以及正无穷大的情况),无法使用 T 类型表示的话,那转换结果取 int 或 long 类型所能表示的最大数字。

从 double 类型到 float 类型做窄化转换的过程与 IEEE 754 中定义的一致,通过 IEEE 754 向最接近数舍入模式(§2.8.1)舍入得到一个可以使用 float 类型表示的数字。如果转换结果的绝对值太小无法使用 float 来表示的话,将返回 float 类型的正负零。如果转换结果的绝对值太大无法使用 float 来表示的话,将返回 float 类型的正负无穷大,对于 double 类型的 NaN 值将就规定转换为 float 类型的 NaN 值。

尽管可能发生上限溢出、下限溢出和精度丢失等情况,但是 Java 虚拟机中数值类型的窄化转换永远不可能导致虚拟机抛出运行时异常(此处的异常是指《Java 虚拟机规范》中定义的异常,请读者不要与 IEEE 754 中定义的浮点异常信号产生混淆)。

对象创建与操作

虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:

指令类型 指令
创建类实例 new
创建数组 newarray,anewarray,multianewarray
访问类字段(static 字段,或者称为类变量)和实例字段(非 static 字段,或者成为实例变量) getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令 baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值储存到数组元素中的指令 bastore、castore、sastore、iastore、fastore、dastore、aastore
取数组长度的指令 arraylength
检查类实例类型的指令 instanceof、checkcas

操作数栈管理指令

Java 虚拟机提供了一些用于直接操作操作数栈的指令。

指令类型 指令
将操作数栈的栈顶一个或两个元素出栈 pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶 dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
将栈最顶端的两个数值互换 swap

控制转移指令

控制转移指令可以让 JVM 有条件或无条件地从指定指令而不是控制转移指令的下一条指令继续执行程序。

指令类型 指令
条件分支 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

在 Java 虚拟机中有专门的指令集用来处理 int 和 reference 类型的条件分支比较操作,为了可以无需明显标识一个实体值是否 null,也有专门的指令用来检测 null 值。

boolean 类型、byte 类型、char 类型和 short 类型的条件分支比较操作,都使用 int 类型的比较指令来完成,而对于 long 类型、float 类型和 double 类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整形值到操作数栈中,随后再执行 int 类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为 int 类型的比较操作,基于 int 类型比较的这种重要性,Java 虚拟机提供了非常丰富的 int 类型的条件分支指令。所有 int 类型的条件分支转移指令进行的都是有符号的比较操作。

方法调用和返回指令

指令方法 指令
指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式 invokevirtual
指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 invokeinterface
指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 invokespecial
指令用于调用类方法(static 方法) invokestatic

而方法返回指令则是根据返回值的类型区分的,包括有 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有一条 return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。

异常处理指令

在程序中显式抛出异常的操作会由 athrow 指令实现,除了这种情况,还有其他的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。

而在 JVM 中,处理异常( catch语句)不是由字节码指令来实现的(很久之前曾经使用 jsr 和 ret 指令来实现,现在已经不用了),而是采用异常表来完成的。

同步指令

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

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持。

结构化锁定(Structured Locking)是指在方法调用期间每一个管程退出都与前面的管程进入相匹配的情形。因为无法保证所有提交给 Java 虚拟机执行的代码都满足结构化锁定,所以 Java 虚拟机允许(但不强制要求)通过以下两条规则来保证结构化锁定成立。假设 T 代表一条线程,M 代表一个管程的话:

  1. T 在方法执行时持有管程 M 的次数必须与 T 在方法完成(包括正常和非正常完成)时释放管程 M 的次数相等。
  2. 找方法调用过程中,任何时刻都不会出现线程 T 释放管程 M 的次数比 T 持有管程 M 次数多的情况。

请注意,在同步方法调用时自动持有和释放管程的过程也被认为是在方法调用期间发生。