JVM类文件结构

时间:2021-02-05 14:03:10

本文将不维护,已迁移至:
http://liucw.cn/2017/12/24/jvm/JVM%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84/

本文是基于周志明的《深入理解Java虚拟机》

  各种不同平台的虚拟机与所有平台都统一使用的程序存储格式:字节码(ByteCode)是构成平台无关性的基石。

一、Class类文件结构

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(比如类或接口也可以通过类加载器直接生成)。

1、Class文件

  1).是一组以8字节为基础单位的二进制流,
  2).各个数据项目严格按照顺序紧凑排列在class文件中,
  3).中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序
  4).当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

高位在前,这种顺序称为“Big-Endian”,具体是指最高位字节在地址最低位,最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节存储顺序,而x86等处理器则是使用了相反的“Little-Endian”顺序来存储数据。

  Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
1).无符号数
  属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。

2).表
  是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。
  表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
  JVM类文件结构
  
  在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。
测试类:

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

  用winhex16进制工具打开后结果如下
  JVM类文件结构

1、魔数
JVM类文件结构
  1).每个Class文件的头4个字节称为魔数(Magic Number)
  2).唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  3).Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。

比特(位)、字节
  比特(位):二进制中最小的单位。每个比特(位)的值要么是0要么是1.
  字节:通常8个比特构成一个字节。

  很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或jpeg等在文件头中都存有魔数。使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变!!!

2、Class文件的版本号
  紧接着魔数的4个字节是Class文件版本号.
  JVM类文件结构
版本号又分为:
  1).次版本号(minor_version): 前2字节用于表示次版本号
  2).主版本号(major_version): 后2字节用于表示主版本号。
  这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。
  0X0034(对应十进制的52):JDK1.8
  0X0033(对应十进制的51):JDK1.7
  0X0032(对应十进制的50):JDK1.6
  0X0031(对应十进制的49):JDK1.5  
  0X0030(对应十进制的48):JDK1.4  
  0X002F(对应十进制的47):JDK1.3  
  0X002E(对应十进制的46):JDK1.2
0X表示16进制

3、常量池
JVM类文件结构
  紧接着魔数与版本号之后的是常量池入口.
  常量池简单理解为class文件的资源从库
  1).是Class文件结构中与其它项目关联最多的数据类型
  2).是占用Class文件空间最大的数据项目之一
  3).是在文件中第一个出现的表类型数据项目。

  由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
  从1开始计数。Class文件结构中只有常量池的容量计数是从1开始的
  第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。但尽管constant_pool列表中没有索引值为0的入口,缺失的这一入口也被constant_pool_count计数在内。例如,当constant_pool中有14项,constant_poo_count的值为15。

常量池之中主要存放两大类常量:
  1).字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  2).符号引用: 属于编译原理方面的概念,包括了下面三类常量:
  ①.类和接口的全限定名
  ②.字段的名称和描述符
  ③.方法的名称和描述符

  Java代码在进行Java编译的时候,并不像C和C++那样有”连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。
  1).constant_pool_count:占2字节,本例为0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~22。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
  2).constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示:
  JVM类文件结构
  省略了显示结果的后半部分,这里可以看到总共有21个常量,并且可以看到常量的类型,如果常量中保存的为索引值(#),也会提示索引指向常量的具体内容(//后的内容),当然其中也包含了很多特殊的符号(如:()V),这些将会在后面的“六,字段表集合”与“七,方法表集合”中进行说明

符号引用与直接引用的关联
  符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
  直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。

4、访问标志(2字节)
JVM类文件结构
  常量池之后的数据结构是访问标志(access_flags),

这个标志主要用于识别一些类或接口层次的访问信息,主要包括:
  1).这个Class是类还是接口
  2).是否定义public
  3).是否定义abstract类型
  4).如果是类的话是否被声明为final等
具体的标志访问如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假
ACC_SYNTHETIC 0x1000 标识别这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

本例测试类的访问标志如何计算的?
  access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。
  由于TestClass只是一个被public修饰的普通Java类,并使用JDK1.2之后的编译器进行编译,因此测试类的访问标志为ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 = 0x0021

5、类索引、父类索引和接口索引集合
JVM类文件结构
  这三项数据主要用于确定这个类的继承关系
  其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是**一组**u2类型的数据。
  类索引(this_class),用于确定这个类的全限定名,占2字节
  父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
  接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节,
  接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中
  this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
  测试类对应的this_class的值为0x0003,即常量池中第3个常量,super_class的值为0x0004,即常量池中的第4个常量,interfaces_counts的值为0x0000,故接口索引集合大小为0
  JVM类文件结构

6、字段表集合
JVM类文件结构
  fields_count:字段表计数器,即字段表集合中的字段表数据个数,占2字节。本测试类其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)
  fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
  在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示,字段表格式如下表所示:

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

  字段表里的字段修饰符放在access_flags中,占2个字节,与类中的访问标志(access_flags)十分相似。
  本例测试类的字段修饰符access_flags,其值为0X0002,由下表的标志位可知这个字段由private修饰

标志名称 标志值 含义
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_TRANSIENT 0x0080 字段是否为transient
ACC_SYNTHETIC 0x1000 字段是否为编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

  当然实际上,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED这3个标志只能选择一个,接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,Class文件对此并无规定,这些都是java语言所要求的
  name_index代表字段的简单名称(参见备注2),占2字节,是一个对常量池的引用 。本例测试类对应的值为0x0005,即常量池中第5个常量
  descriptor_index代表代表参数的描述符(参见备注3),占2个字节,是一个对常量池的引用,其值为0x0006,即常量池中第6个常量
  JVM类文件结构
  综上,可以推断出源代码定义的字段为(和测试类完全一样):
private int m;
  字段表包含的固定数据项到descriptor_index结束,之后跟随一个属性表集合用于存储一些附加信息:
  attributes_count(属性计数器,占2字节。本例的测试类值为0x0000,对应字段m,它的属性表计数器为0,所以该字段没有额外需要描述的信息)
  attributes(属性表集合,详细说明见后面“八、属性表集合”)

  字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段
  Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的

7、方法表集合
JVM类文件结构
  methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法
  methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样:

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

  由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_NATIVE标志、ACC_STRICTFP标志和ACC_ABSTRACT标志
第一个方法(由编译器自动添加的默认构造方法):
JVM类文件结构
  access_flags为0x0001,即public;name_index为0x0007,即常量池中第7个常量;descriptor_index为0x0008,即常量池中第8个常量
  JVM类文件结构
  JVM类文件结构
  接下来2个字节为属性计数器,其值为0x0001,说明这个方法的属性表集合中有一个属性(详细说明见后面“八、属性表集合”)
  属性名称为接下来2位0x0009,指向常量池中第9个常量:Code。
  接下来4位为0x0000002F,表示Code属性值的字节长度为47。
  接下来2位为0x0001,表示该方法的操作数栈的深度最大值为1。
  接下来2位依然为0x0001,表示该方法的局部变量占用空间为1。
  接下来4位为0x0000005,则紧接着的5个字节0x2AB70001B1为该方法编译后生成的字节码指令(各字节对应的指令不介绍了,可查询虚拟机字节码指令表)。
  接下来2个字节为0x0000,说明Code属性异常表集合为空。
  JVM类文件结构
  接下来2个字节为0x0002,说明Code属性带有2个属性,
  接下来2位0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
  接下来4位为0x00000006,表示LineNumberTable属性值所占字节长度为6。
  接下来2位为0x0001,即该line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0003,LineNumberTable属性结束。
  JVM类文件结构
  接下来2位0x000B为Code属性第二个属性的属性名,指向常量池中第11个常量:LocalVariableTable。该属性值所占的字节长度为0x0000000C=12。
  接下来2位为0x0001,说明local_variable_table中只有一个local_variable_info表,按照local_variable_info表结构,start_pc为0x0000,length为0x0005,name_index为0x000C,指向常量池中第12个常量:this,descriptor_index为0x000D,指向常量池中第13个常量:LTestClass;,index为0x0000。
第一个方法结束
JVM类文件结构

第二个方法:
JVM类文件结构
  access_flags为0x0001,即public;
  name_index为0x000E,即常量池中第14个常量;
  descriptor_index为0x000F,即常量池中第15个常量
JVM类文件结构
JVM类文件结构
  接下来2个字节为属性计数器,其值为0x0001,说明这个方法有一个方法属性,
  接下来2位0x0009,指向常量池中第9个常量:Code。
  接下来4位为0x00000031,表示Code属性值的字节长度为49。
  接下来2位为0x0002,表示该方法的操作数栈的深度最大值为2。
  接下来2位为0x0001,表示该方法的局部变量占用空间为1。
  接下来4位为0x0000007,则紧接着的7个字节0x2AB400120460AC为该方法编译后生成的字节码指令。
  接下来2个字节为0x0000,说明Code属性异常表集合为空。
JVM类文件结构
  接下来2个字节为0x0002,说明Code属性带有2个属性,
  接下来2位0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
  接下来4位为0x00000006,表示LineNumberTable属性值所占字节长度为6。
  接下来2位为0x0001,即该line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0006,LineNumberTable属性结束。
JVM类文件结构
  和第一个方法的LocalVariableTable属性基本相同,唯一的区别是局部变量this的作用范围覆盖的长度为7而不是5。
第二个方法结束
  如果子类没有重写父类的方法,方法表集合中就不会出现父类方法的信息;有可能会出现由编译器自动添加的方法(如:最典型的< init>,实例类构造器)
  在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名(,由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存

注意:Java代码的方法特征签名只包括方法名称、参数顺序、参数类型。而字节码的特征签名还包括方法返回值和受异常表。

  javap工具在后半部分会列出分析完成的方法(可以看到和我们的分析结果是一样的):
JVM类文件结构

8、属性表集合
JVM类文件结构
  起始2位为0x0001,说明有一个类属性。
  接下来2位为属性的名称,0x0010,指向常量池中第16个常量:SourceFile。
  接下来4位为0x00000002,说明属性体长度为2字节。
  最后2个字节为0x0011,指向常量池中第27个常量:TestClass.java,即这个Class文件的源码文件名为TestClass.java

  前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。
  与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类文件、字段表、方法表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTale Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类文件、方法表、字段表 标识方法或字段是由编译器自动生成的

  上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,比如一个方法如下:

PS:
  1,全限定名:将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:”com.test.Test”类的全限定名为”com/test/Test;”
  2,简单名称:没有类型和参数修饰的方法或字段名称。例如:”public void add(int a,int b){…}”该方法的简单名称为”add”,”int a = 123;”该字段的简单名称为”a”
  3,描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如:Ljava/lang/Object;

  对于数组类型,每一维将使用一个前置的“[”字符来描述,如:”int[]”将被记录为”[I”,”String[][]”将被记录为”[[Ljava/lang/String;”
  用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组”()”之内,如:方法”String getAll(int id,String name)”的描述符为”(I,Ljava/lang/String;)Ljava/lang/String;”
  4,Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot


二、字节码指令简介

  Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
  如果忽略异常处理,那 Java 虚拟机的解释器使用下面这个伪代码的循环即可有效地工作:

do { 
  自动计算 PC 寄存器以及从 PC 寄存器的位置取出操作码;
  if (存在操作数) 取出操作数;
  执行操作码所定义的操作
} while (处理下一次循环);

do {
  自动计算 PC 寄存器以及从 PC 寄存器的位置取出操作码 ;
  if ( 存在操作数 ) 取出操作数 ;
  执行操作码所定义的操作
} while ( 处理下一次循环 ) ;

  操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那它将会以 Big-Endian 顺序存储——即高位在前的字节序。举个例子,如果要将一个 16 位长度的无符号整数使用两个无符号字节存储起来(将它们命名为 byte1 和 byte2),那它们的值应该是这样的:
  (byte1 << 8) | byte2
  ( byte1 << 8 )| byte2
  字节码指令流应当都是单字节对齐的,只有“tableswitch”和“lookupswitch”两条指令例外,由于它们的操作数比较特殊,都是以 4 字节为界划分开的,所以这两条指令那个也需要预留出相应的空位来实现对齐。
  限制 Java 虚拟机操作码的长度为一个字节,并且放弃了编译后代码的参数长度对齐,是为了尽可能地获得短小精干的编译代码,即使这可能会让 Java 虚拟机的具体实现付出一定的性能成本为代价。由于每个操作码只能有一个字节长度,所以直接限制了整个指令集的数量 (字节码无法超过 256 条的限制就来源于此) ,又由于没有假设数据是对齐好的,这就意味着虚拟机处理那些超过一个字节的数据的时候,不得不在运行时从字节中重建出具体数据的结构,这在某种程度上会损失一些性能。

数据类型与 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)。
Java 虚拟机指令集所支持的数据类型:
opcode byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore aastore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idiv ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tand iand land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l f2d
d2T d2i d2l d2f
Tcmp lcmp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn
  在 Java 虚拟机中,实际类型与运算类型之间的映射关系,如下表所示:
实际类型 运算类型 分类
boolean int 分类一
byte int 分类一
char int 分类一
short int 分类一
int int 分类一
float float 分类一
reference reference 分类一
returnAddress returnAddress 分类一
long long 分类二
double double 分类二
  有部分对操作栈进行操作的 Java 虚拟机指令(例如 pop 和 swap 指令)是与具体类型无关的,不过这些指令也必须受到运算类型分类的限制,这些分类也在表中列出了。

加载和存储指令
  加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输:
  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_,它代表了 iload_0、iload_1、iload_2 和 iload_3 这几条指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都是在指令中隐含的。除此之外,他们的语义与原生的通用指令完全一致(例如 iload_0 的语义与操作数为 0 时的 iload 指令语义完全一致)。在尖括号之间的字母制定了指令隐含操作数的数据类型,代表是 int 形数据,代表 long 型,代表 float 型,代表 double型。在操作 byte、char 和 short 类型数据时,也用 int 类型表示。
这种指令表示方法,在整个《Java 虚拟机规范》之中都是通用的。

运算指令
  算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是那种算术指令,都是使用 Java 虚拟机的数字类型的。数据没有直接支持 byte、short、char 和 boolean 类型(§2.11.1)的算术指令,对于这些数据的运算,都是使用操作 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,§2.3.2)和逐级下溢(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 的向零舍入模式(§2.8.1)
取整,获得整数值 v,这时候可能有两种情况:
    - 如果 T 是 long 类型,并且转换结果在 long 类型的表示范围之内,那就转换为 long
类型数值 v
    - 如果 T 是 int 类型,并且转换结果在 int 类型的表示范围之内,那就转换为 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。

控制转移指令
  控制转移指令可以让 Java 虚拟机有条件或无条件地从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括有:
  * 条件分支: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 类型的条件分支转移指令进行的都是有符号的比较操作。
  
方法调用和返回指令
以下四条指令用于方法调用:
  * invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
  * invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  * invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  * invokestatic 指令用于调用类方法(static 方法)。
  
  而方法返回指令则是根据返回值的类型区分的,包括有 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有一条 return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。

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

同步
  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 次数多的情况。
  
请注意,在同步方法调用时自动持有和释放管程的过程也被认为是在方法调用期间发生。