Java虚拟机指令的组成:操作码(Opcode,一个字节长度的、代表着某种特定操作含义的数字)+多个操作数(Operands,此操作所需的参数)。Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数指令都不包含操作数。

字节码指令集的特点:A、指令集的操作码总数不可能超过256条;B、当数据大小超过一个字节时,Java虚拟机需要重构出具体数据的结构。(比如:将一个16位长度的无符号整数使用两个无符号字节(byte1,byte2)存储起来,那它们的值应该是(byte1<<8)|byte2)

字节码指令集的优点:A、放弃了操作数长度对齐,意味着可以节省很多填充和间隔符号;B、用一个字节代表操作码可以获得尽可能短小精干的编译代码。——数据量小、传输效率高。

字节码指令集的不足:在解释执行字节码时会损失一些性能。(以时间换空间)

Java虚拟机解释器的执行模型:

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

一、字节码和数据类型

大多数的指令都包含了其操作所对应的数据类型信息。例如,iload指令用于从局部变量表中加载int类型的数据到操作数栈中,fload指令用于从局部变量表中加载float类型的数据到操作数栈中。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中他们必须拥有各自独立的操作码。——可能是因为取数据的问题,int类型的数据在Class文件中的存储格式和float会有一定的区别。

1、与数据类型相关的指令:它们的操作码助记符中都有特殊的字符来表示专门为哪种数据类型服务(i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference)。也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。

2、与数据类型无关的指令:如goto指令。

注意点:如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数量恐怕就会超出一个字节所能表示的数量范围了。因此Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,换句话说,指令集将会被故意设计成非完全独立的。——也就是并不是每一条操作都定义了与每一个数据类型相关的特定指令。

实际上大部分指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译器或者运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零扩展为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。

接下来看一下九大类指令

二、加载和存储指令

1、作用:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输

2、内容:

#将一个局部变量加载到操作数栈:load指令

#将一个数值从操作数栈存储到局部变量表:store指令

#将一个常量加载到操作数栈:push指令、dc指令、const指令

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

3、存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。

三、运算指令

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

2、划分——都使用操作int类型的指令代替

2.1.对整型数据进行运算的指令

2.2.对浮点类型数据进行运算的指令

3、内容

#加法指令:add

#减法指令:sub

#乘法指令:mul

#除法指令:div

#求余指令:rem

#取反指令:neg

#位移指令:shl(逻辑左移)、shr(逻辑右移)、ushr(算术右移)

#按位或指令:or

#按位与指令:and

#按位异或指令:xor

#局部变量自增指令:inc

#比较指令:cmpg、cmpl、cmp

4、异常情况:仅规定了在处理整型数据时只有除法指令以及求余指令中出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算场景都不应该抛出运行时异常。当一个操作产生溢出时,将会使用有符号的无穷大来表示(NaN)。在对long类型的数据进行比较时,虚拟机采用带符号的比较方式,而在对浮点数值精心比较时,虚拟机采用IEEE 754规范所定义的无信号比较方式。

四、类型转换指令

1、作用:可以将两种不同的数值类型精心相互转换

2、Java虚拟机直接支持以下数值类型的宽化类型转换(小范围到大范围)

#int类型到long、float或者double

#long类型到float、double

#float类型到double

3、窄化类型转换必须显式地使用转换指令来完成:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转化过程很可能会导致数值精度的丢失。——直接丢弃多出来的高位。

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

#如果浮点值是NaN,那转换结果就是int或long中的0;

#如果浮点值不是无穷大的话,浮点值使用向零舍入模式取整,获取整数值v,如果v在目标T的表示范围内,那么结果就是T;

#否则,将根据v的符号,转换为T所能表示的最大或最小整数。

Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。

五、对象创建和访问指令

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

#创建类实例的指令:new

#创建数组的指令:newarray、anewarray、multianewarray

#访问类变量(static字段)和实例变量(非static字段)的指令:getfield、putfield、getstatic、putstatic

#把一个数组元素加载到操作数栈:aload

#将一个操作数栈的值存储到数组元素中的指令:astore

#取数组长度的指令:Arraylength

#检查类实例类型的指令:instanceof、checkcast

六、操作数栈管理指令

1、作用:如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令

2、内容:

#将操作数栈的栈顶一个或两个元素出栈:pop、pop2

#复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2

#将栈最顶端的两个数值交换:swap

七、控制转移指令

1、作用:让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。

2、内容:

#条件分支:ifxx等

#符合条件分支:tableswitch、lookupswitch

#无条件分支:goto、jsr、ret

八、方法调用和返回指令

1、方法调用指令

#invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派

#invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用

#invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

#invokestatic指令用于调用类方法(static)

#invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

2、方法返回指令

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型进行区分的,包括ireturn(返回值为boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法<init>以及类和接口的类初始化方法<clinit>使用。

九、异常处理指令

在Java程序中显示抛出异常的操作都由athrow指令来实现,除了用throw语句显示抛出异常外,Java虚拟机还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。(如除数为0的异常)

而在Java虚拟机中,处理异常(catch语句)不是由2字节码指令来实现的,而是采用异常表来完成的。

十、同步指令

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

1、方法级的同步:隐式的,即无需通过字节码指令控制,它实现在方法调用和返回操作之中。

当方法调用时,调用指令将会检查方法的ACC_SYNCRONIZED访问标志是否被设置了,如果是,执行程序就要求先成功持有管程,然后才能执行方法,最后当方法执行完成时(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果执行期间出现了方法内部无法解决的异常,那么这个方法所持有的管程将在异常抛出到同步方法之外时自动释放。

2、同步一段指令集序列:syncronized

Java虚拟机中有monitorenter和monitorexit两条指令来支持syncronized关键字的语义,正确实现syncronized关键子,需要javac编译器与java虚拟机两者共同协作。编译器必须保证无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法时正常结束还是异常结束。

 

十一、后续:公有设计和私有实现

Java虚拟机规范描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集。这些内容与硬件、操作系统及具体的Java虚拟机实现之间是完全独立的,虚拟机实现者可能更愿意把它们看做是程序在各种Java平台实现之间互相安全地交互的手段。

公有设计和私有设计的分界线——虚拟机后台如何处理Class文件完全可以由实现者自己去定义,只要它在外部接口上看起来与规范描述的一致即可。

虚拟机实现的方式主要有以下两种:

1、将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集

2、将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)