由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
Java 刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码。
字节码(ByteCode)是构成平台无关的基石。虚拟机的语言无关性也越来越被开发者所重视,JVM 设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在 JVM 上运行的语言,比如 Clojure、Groovy、JRuby、Jython、Scala。
实现语言无关性的基础仍是虚拟机和字节码存储格式,Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与 Class 文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成 Class 文件,从而在虚拟机上运行。
Class 文件是一组以 8 个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符。
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,其中只有两种数据类型:
无符号数属于基本的数据类型,以 u1、u2、u4 和 u8 来分别代表 1 个字节、2 个字节、4 个字节和8 个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯以“_info”结尾。表用于描述有层次关系的复合结构数据,整个 Class 文件本质上就是一张表。
Class 文件格式:
无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
写一个简单的程序来分析 Class 文件结构:
package com.chen;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
通过 WinHex 打开 Class 文件:
Class文件的头 4 个字节被称为魔数 (Magic Number),唯一作用是确定文件是否为一个可被虚拟机接受的 Class 文件,固定为“0xCAFEBABE”。
第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version),Java 版本号是从 45 开始的,JDK 1.1 之后每个大版本发布,主版本向上加1(JDK 1.0~1.1使用了 45.0~45.3)。Java 能向下兼容之前的版本,无法运行后续的版本。我们举的例子中,0x0034 即 52,对应 JDK 1.8。
紧接着就是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一。
由于常量池中的常量数量不固定,因此需要在常量池前放置一项 u2 类型的数据,来表示常量池容量计数值(constant_pool_count)。该值是从 1 开始的,例子的 0x0016 为十进制的 22,代表常量池中有 21 项常量,索引值范围为1~21。设计者将第 0 项常量空出来,目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References):
Java 代码在 javac 编译时不会有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。所以 Class 文件不会保存各个方法、字段和最终内存布局信息,必须经过运行期转换,才能得到真正内存入口地址。当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
常量池中每一项常量都是一个表,JDK 1.7 中常量池共有 14 种不同的表结构数据,这些表结构开始的第一位是一个 u1 类型的标志位,代表当前常量的类型,具体如下图所示:
结合下图中各个表结构的说明和之前使用 javap 解析的文件内容:
例子中,第一个常量是 0x0A,属于 CONSTANT_Methodref_info 类型,0x0004,指向第四项 CONSTANT_Class_info 常量,而0x0012 指向第18项 CONSTANT_NameAndType_info 常量。
第二个常量是 0x09,对应的是 CONSTANT_Fieldref_info 类型,0x0003,指向第三项 CONSTANT_Class_info 常量,而0x0013指向19项 CONSTANT_NameAndType_info 常量。
其余的常量都可以通过类似的方法计算出来,计算机可以帮我们完成这一步,通过 javap,使用带 -verbose 参数输出的字节码内容:
从图中可以看出,有一些常量从来没有在代码出现过,如“I”、“V”、“”、“LineNumberTable”、“LocalVariableTable”等,这些自动生成的常量,会在字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,用来描述一些不方便使用“固定字节”进行表述的内容。
在常量池结束之后,接着就是两个字节的访问标志(acess_flags),用于识别一些类或者接口层次的信息。access_flags 中一共有16个标志位可以使用,当前只定义了 8 个,没有使用的一律为 0。
具体的标志位:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 是否为 Public 类型 |
ACC_FINAL | 0x00 10 | 是否被声明为 final,只有类可以设置 |
ACC_SUPER | 0x00 20 | 是否允许使用 invokespecial 字节码指令的新语意,invokespecial 在JDK 1.0.2 发生过改变,在 JDK 1.0.2 之后编译出来的类这个标志都必须为真 |
ACC_INTERFACE | 0x02 00 | 标志这是一个接口 |
ACC_ABSTRACT | 0x04 00 | 是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x10 00 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x20 00 | 标志这是一个注解 |
ACC_ENUM | 0x40 00 | 标志这是一个枚举 |
我们例子中的 TestClass 是一个普通的 Java 类,不是接口,注解或枚举,被 public 修饰但不是 final 和 abstract,所以它的 ACC_PUBLIC、ACC_SUPER 为真,而 ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM六个标志位都应该为假,所以它的 acess_flags 应该为 0x0001|0x0020=0x0021,如图,偏移地址:000000D9
这三个索引用来确定这个类的继承关系:
字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括**方法内部声明的局部变量,**它不会列出从父类和超类继承而来的字段,但有可能列出原本 Java 代码中不存在的字段,如在内部类中为了保持对外部类的访问性所添加指向外部类实例的字段。并且在 Java 语言中字段是无法重载的,但对于字节码来说,字段的描述符不同,字段的重名是可行的。
字段表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中字段修饰符放在 access_flags 项目中,与类中 access_flags 类似,都是 u2 数据类型,可以设置的标志位和含义:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSTENT | 0x0080 | 字段是否为transient |
ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 最多只能三选一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口中字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。
access_flags 之后是 name_index 和 descriptor_index 。它们是对常量池的引用,分别代表着字段的简单名称以及字段方法和方法的描述符。
描述符是用来描述字段的数据类型、方法的参数列表和返回值。描述符规则:
标志符 | 含义 |
---|---|
B | 基本数据类型 byte |
C | 基本数据类型 char |
D | 基本数据类型 double |
F | 基本数据类型 float |
I | 基本数据类型 int |
J | 基本数据类型 long |
S | 基本数据类型 short |
Z | 基本数据类型 boolean |
V | 基本数据类型 void |
L | 对象类型,如 Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的“[”字符来描述.如一个定义为"java.lang.Stirng[][]"类型的二维数组,将被记录为:“[[Ljava/lang/Stirng;”,一个整型数组“int[]”将被记录为“[I”。
描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内,如方法 void inc()
描述符为 “()V”,方法 java.lang.String toString()
描述符为 “()Ljava/lang/String;”,方法为 int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromindex)
的描述符为“([CII[CIII)I”。
private int m;
在 descriptor_index 之后跟随着一个属性表集合(attribute_info)用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。对于本例中的字段 m,它的属性计数器为 0,没有需要额外描述的信息,如果字段 m 的声明改为 final static int m = 1;
,那就可能会存在一项 ConstantValue 的属性,其值指向常量 1。
和字段表类似,方法表的结构依次包括访问标志(acess_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
volatile 和 transient 不能修饰方法,但 synchronized、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的标志位:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYHCHRONRIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是有编译器产生的方法 |
ACC_VARARGS | 0x0080 | 方法是否接受参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是有编译器自动产生的 |
在例子中:
第一个 u2 类型的数据(计数器)为 0x0002,代表集合有两个方法(分别为编译器添加的实例构造器
和源码中的 inc()
)。
第二个方法,inc()
方法:
与字段表集合对应的,如果父类方法没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样会出现由编译器自动添加的方法,最典型的是类构造器
方法和实例构造器
方法。
Java 语言中无法通过返回值不同来对一个已有方法进行重载的,但在 Class 文件结构中,只要描述符不同的两个方法也可以共存。也就是说,返回值不同,也可以 共存于同一个 Class 文件中。
在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表集合不要求各个属性表的严格顺序,只要不要已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息,JVM 运行时会忽略不认识的属性。
虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
属性表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
Java 程序方法体中的代码经过 Javac 编译器处理后,变为字节码指令存储在 Code 属性内。并非所有方法表都存在这个属性,比如接口或抽象类中的方法。
Code 属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
本例中的第一个方法
,对应字节码:
其中
方法的符号引用通过 javap 得到另一个方法:
其中 args_size = 1 ,因为实例方法,都可以通过 “this” 关键字访问到此方法所属的对象。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值也会从 1 开始计算,这个处理只对实例方法有效。
在字节码指令之后是这个方法的显示异常处理表集合,异常表对于 Code 属性并不是必须存在的。
异常表格式:
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
这四个字段,如果当字节码在第 start_pc 行到 end_pc行(不含第 end_pc 行)出现类型为 catch_type 或者其子类的异常,则转到第 handler_pc 行继续处理,当 catch_type 的值为 0 ,代表任意异常情况都需要转到 handler_pc 处来进行处理。
编译器通过异常表来实现 Java 异常以及 finally 处理机制。
Exceptions 属性列举出方法中基本抛出的受查异常(Checked Exception),也就是方法描述时 throws 关键字后面列举的异常,和 Code 属性里的异常表不同。
其属性表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_lrngth | 1 |
u2 | attribute_of_exception | 1 |
u2 | exception_index_tsble | number_of_exceptions |
Exceptions 属性中的 number_of_exceptions 项表示方法可能抛出 number_of_exceptions 种受查异常,每一种受查异常使用一个 exception_index_table 项表示,exception_index_table 是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该受查异常的类型。
LineNumberTable 属性用于描述 Java 源代码行号与字节码行号(字节码偏移量)的对应关系。它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:lines 选项来取消或生成这项信息。如果没有这个属性,运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。
结构:
类型 | 名称 | 数量 |
---|---|---|
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 |
它是用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:vars 选项来取消或生成这项信息。如果没有这个属性,所有的参数名称都会丢失,取之以 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 | 指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量名称 |
u2 | descriptor_index | 1 | 指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量描述符 |
u2 | index | 1 | 局部变量在栈帧局部变量表中 Slot 的位置,如果这个变量的数据类型为 64 位类型(long或double),它占用的 Slot 为 index 和 index+1 这 2 个位置 |
SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。可以使用 Javac 的 -g:none 和 -g:source 来关闭或生成。对于大多数类来说,类名和文件名相同,但有些例外情况(如内部类)例外。如果不生成这项属性,当抛出异常,堆栈将不会显示出错代码所属的文件名。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index 数据项指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源码文件的文件名。
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。
只有被 static 关键字修饰的变量才可以用这个属性。对于非 static 类型的变量的赋值是在实例构造器
方法中进行的。而对于类变量有两种方式:在类构造器方法中或者使用 ConstantValue 属性。
目前 Sun Javac 编译器的选择是:
方法中初始化虚拟机规范中并没有强制要求要求 ConstantValue 属性的字段必须设置 ACC_FINAL 标志,只是必须设置 ACC_STATIC 标志而已。对 final 关键字的要求是 Javac 编译器自己加入的限制。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
ConstantValue 属性是一个定长属性,其 attribute_length 数据项值必须固定为 2。constantvalue_index 代表常量池的一个字面量引用。
InnerClasses 属性用于记录内部类与宿主类之间的关联。
属性结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_class | number_of_classes |
number_of_classes 记录多少个内部类信息,每一个内部类信息由一个 inner_classes_info 表描述,其结构:
类型 | 名称 | 数量 | 作用 |
---|---|---|---|
u2 | inner_classes_info_index | 1 | 指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的符号引用 |
u2 | outer_classes_info_index | 1 | 指向常量池 CONSTANT_Utf8_info 类型常量的索引,宿主类的符号引用 |
u2 | inner_name_index | 1 | 指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的名称,如果是匿名内部类,则为 0 |
u2 | inner_class_access_flags | 1 | 内部类的访问标志 |
inner_class_access_flags 标志:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 内部类是否为public |
ACC_PRIVATE | 0x0002 | 内部类是否为private |
ACC_PROTECTED | 0x0004 | 内部类是否为protected |
ACC_STATIC | 0x0008 | 内部类是否为static |
ACC_FINAL | 0x0010 | 内部类是否为final |
ACC_INTERFACE | 0x0020 | 内部类是否为接口 |
ACC_ABSTRACT | 0x0400 | 内部类是否为抽象类 |
ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 |
ACC_ANNOTATION | 0x2000 | 内部类是否为注解 |
ACC_ENUM | 0x4000 | 内部类是否为枚举 |
这两个都属于标志类型的布尔属性。
Deprecated 表示某个类、字段或方法,已经被程序作者定义为不再推荐使用,可以在代码通过 @deprecated
标识。
Synthetic 表示字段或方法不是由Java源码直接产生,而是编译器自行添加的,唯一例外是实例构造器
和类构造器
。
Deprecated 和 Synthetic 属性结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
attribute_length 数据项值必须为 0x00000000,因为不需要属性值设置
这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_entries | 1 |
stack_map_frame | stack_map_frame entries | number_of_entries |
一个可选的定长属性,在 JDK 1.5 发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。这主要是因为 Java 的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过 Signature 属性,Java 的反射 API 能够获取泛型类型。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_index | 1 |
其中 signature_index 值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示类签名、方法类型签名或字段类型签名。
在JDK 1.7 之后加入到 Class 文件规范中。它是一个复杂的变长属性,位于类文件的属性表中,用于保存 invokedynamic
指令引用的引导方法限定符。
JVM 的指令由一个字节长度、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,指令包括:
iload/iload
等(加载局部变量到操作栈)istore/istore
等(从操作数栈存储到局部变量表)bipush/sipush/ldc/iconst_
(加载常量到操作数栈)wide
(扩充局部变量表访问索引)其中部分指令(如 iload_
),代表一组指令(如 iload_0、iload_1、iload_2、iload_3
),它们都是带有一个操作数的通用指令,它们省略了显式的操作数,如 iload_0
与操作数为 0 的 iload
指令语义完全一致。
###运算指令
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。没有直接支持 byte、short、char 和 boolean 类型的算术指令而采用 int 代替。
iadd/isub/imul/idiv
irem
ineg
ishl/ishr
ior
iand
ixor
iinc
dcmpg/dcmpl/fcmpg/fcmpl/lcmp
类型转换指令可以将两种不同的数值类型进行互相转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。JVM 直接支持宽化类型转换(Widening Numeric Conversions,小范围向大范围的安全转换):
处理窄化类型转换(Narrowing Numberic Conversions)时,必须显式使用转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f
。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,可能会导致精度丢失。
虽然类实例和数组都是对象,但是虚拟机创建类对象和数组的指令是不同的。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或数组元素,指令如下:
new
;newarray、anewarray、multianewarray;
getfield、putfield、getstatic、putstatic
;baload、caload、saload、iaload、laload、faload、daload、aaload
;bastore、castore、sastore、iastore、fastore、dastore、aastore
;arraylength
;instanceof、checkcast
;就像操作一个普通的栈一样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:
pop、pop2
;dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
;swap
;控制转移指令可以让 JVM 有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行,可以理解为控制转移指令改变了 PC 寄存器的值。指令如下:
ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne
;tableswitch、lookupswitch
;goto、goto_w、jsr、jsr_w、ret
;这里仅仅列出 5 条用于方法调用的指令:
invokevirtual
指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式;invokeinterface
指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用;invokespecial
指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法;invokestatic
指令用于调用类方法(static方法);invokedynamic
指令用于在运行时动态解析出调用点限定符索引用的方法,并执行方法,前面 4 条指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的;方法调用指令与类型无关,但是方法返回指令是根据返回值的类型区分的,包括 ireturn、lreturn、freturn、dreturn 和 areturn
,另外还有一个 return
指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。
在 Java 程序中显式抛出异常的操作(throw 语句)都是由 athrow 指令来实现的,除了用 throw 语句显式抛出异常外,Java 虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。
而在 Java 虚拟机中,处理异常(catch语句)不是由字节码指令来完成的,而是采用异常表来完成的。
Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式的,即不需要通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令就会去检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置了,如果设置,执行线程就要求持有管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个方法在执行期间发生了异常,并在方法中无法处理次异常,那么这个同步方法所持有的管程将在异常抛出后自动释放。
同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块表示的,Java 虚拟机的指令集中有 monitorenter
和monitorexit
指令来支持 synchronized 关键字的语义。正确实现 synchronized 关键字需要 Javac 编译器和 Java 虚拟机两者共同协作。编译器必须保证每个 monitorenter
指令都有对应的 monitorexit
指令。
JVM 规范描述了共同程序存储格式:Class 文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的 JVM 实现之间是完全独立的,虚拟机实现者更愿意把它们看做是程序在各种 Java 平台之间互相安全的交互的手段。
Java 虚拟机的实现必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的含义。但一个优秀的虚拟机实现,在满足虚拟机规范的约束下具体实现做出修改和优化也是完全可行,甚至是被鼓励的。虚拟机后台如何处理 Class 文件并不关心,只要外部接口的表现与规范描述的一致即可。
虚拟机实现的方式主要有两种:
Class 文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容。Class 文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是 Java 技术体系实现平台无关、语言无关两项特性的重要支柱。
本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。