代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.类型转换指令
宽化类型转换(Widening Numeric Conversions)
1.转换规则:
Java虚拟机直接支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:
- 从int类型到long、float或者double类型。对应的指令为: i2l、i2f、i2d
- 从long类型到float、double类型。对应的指令为:l2f、l2d
- 从float类型到double类型。对应的指令为:f2d
- 简化为:int –> long –> float –> double
2. 精度损失问题
2.1 宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到 long,或者从int转换到double.都不会丢失任何信息,转换前后的值是精确相等的。
2.2 从int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。
窄化类型转换(Narrowing Numeric Conversion)
转换规则
Java虚拟机也直接支持以下窄化类型转换:
- 从int类型至byte、short或者char类型。对应的指令有:i2b、i2s、i2c
- 从long类型到int类型。对应的指令有:l2i
- 从float类型到int或者long类型。对应的指令有:f2i、f2l
- 从double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f
类型转换指令大致对应Java语言中的基本类型强制转换操作。类型转换指令有共15条,将全部在本节实现。引用类型转换对应的是checkcast指令,将在第6章介绍。按照被转换变量的类型,类型转换指令可以分为3种:i2x系列指令把int变量强制转换成其他类型;l2x系列指令把long变量强制转换成其他类型;f2x系列指令把float变量强制转换成其他类型;d2x系列指令把double变量强制转换成其他类型。以d2x系列指令为例进行讨论。
整体代码
在instructions\conversions目录下创建d2x.go文件,在其中定义d2f、d2i和d2l指令,代码如下:
package conversions
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Convert double to float
type D2F struct{ base.NoOperandsInstruction }
func (self *D2F) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
d := stack.PopDouble()
f := float32(d)
stack.PushFloat(f)
}
// Convert double to int
type D2I struct{ base.NoOperandsInstruction }
func (self *D2I) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
d := stack.PopDouble()
i := int32(d)
stack.PushInt(i)
}
// Convert double to long
type D2L struct{ base.NoOperandsInstruction }
func (self *D2L) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
d := stack.PopDouble()
l := int64(d)
stack.PushLong(l)
}
在instructions\conversions目录下创建f2x.go文件,代码如下:
package conversions
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Convert float to double
type F2D struct{ base.NoOperandsInstruction }
func (self *F2D) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
f := stack.PopFloat()
d := float64(f)
stack.PushDouble(d)
}
// Convert float to int
type F2I struct{ base.NoOperandsInstruction }
func (self *F2I) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
f := stack.PopFloat()
i := int32(f)
stack.PushInt(i)
}
// Convert float to long
type F2L struct{ base.NoOperandsInstruction }
func (self *F2L) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
f := stack.PopFloat()
l := int64(f)
stack.PushLong(l)
}
在instructions\conversions目录下创建i2x.go文件,代码如下:
package conversions
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Convert int to byte
type I2B struct{ base.NoOperandsInstruction }
func (self *I2B) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
b := int32(int8(i))
stack.PushInt(b)
}
// Convert int to char
type I2C struct{ base.NoOperandsInstruction }
func (self *I2C) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
c := int32(uint16(i))
stack.PushInt(c)
}
// Convert int to short
type I2S struct{ base.NoOperandsInstruction }
func (self *I2S) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
s := int32(int16(i))
stack.PushInt(s)
}
// Convert int to long
type I2L struct{ base.NoOperandsInstruction }
func (self *I2L) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
l := int64(i)
stack.PushLong(l)
}
// Convert int to float
type I2F struct{ base.NoOperandsInstruction }
func (self *I2F) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
f := float32(i)
stack.PushFloat(f)
}
// Convert int to double
type I2D struct{ base.NoOperandsInstruction }
func (self *I2D) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
i := stack.PopInt()
d := float64(i)
stack.PushDouble(d)
}
在instructions\conversions目录下创建l2x.go文件,代码如下:
package conversions
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Convert long to double
type L2D struct{ base.NoOperandsInstruction }
func (self *L2D) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
l := stack.PopLong()
d := float64(l)
stack.PushDouble(d)
}
// Convert long to float
type L2F struct{ base.NoOperandsInstruction }
func (self *L2F) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
l := stack.PopLong()
f := float32(l)
stack.PushFloat(f)
}
// Convert long to int
type L2I struct{ base.NoOperandsInstruction }
func (self *L2I) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
l := stack.PopLong()
i := int32(l)
stack.PushInt(i)
}
代码解释
以d2i指令为例,它的Execute()方法如下:

因为Go语言可以很方便地转换各种基本类型的变量,所以类型转换指令实现起来还是比较容易的。
2.比较指令
比较指令的说明
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
- 比较指令有: dcmpg, dcmpl、fcmpg、fcmpl、lcmp.与前面讲解的指令类似。首字符d表示double类型,f表示float,l表示long.
- 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。
- 指令dempl和dempg也是类似的,根据其命名可以推测其含义,在此不再赘述。
- 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。
举例:指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈项的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0:若v1>v2则压入1:若v1<v2则压入-1.
两个指令的不同之处在于,如果遇到NaN值, fcmpg会压入1,而fcmpl会压入-1.
一、条件跳转指令
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈项元素的准备,然后进行条件跳转。
条件跳转指令有: ifeq,iflt,ifle, ifne,ifgt, ifge, ifnull,ifnonnull.这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。
具体说明:

注意:
1. 与前面运算规则一致:
- 对于boolean、 byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
- 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
2.由于各类型的比较最终都会转为int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强大的。
二、比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有: if_icmpeq、 if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne.
其中指令助记符加上”if_”后,以字符”i”开头的指令针对int型整数操作(也包括short和byte类型),以字符”a”开头的指令表示对象引用的比较。

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
三、多条件分支跳转指令
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。

从助记符上看,两者都是switch语句的实现,它们的区别:
- tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
- 指令lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。

例如:

四、无条件跳转指令
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且己经被虚拟机逐渐废弃,故不在这里介绍这两个指令。
比较指令可以分为两类:一类将比较结果推入操作数栈顶,一类根据比较结果跳转。比较指令是编译器实现if-else、for、while等语句的基石,共有19条,将全部在本节实现。
lcmp指令
lcmp指令用于比较long变量。
整体代码
在instructions\comparisons目录下创建lcmp.go文件,在其中定义lcmp指令,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Compare long
type LCMP struct{ base.NoOperandsInstruction }
func (self *LCMP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
v2 := stack.PopLong()
v1 := stack.PopLong()
if v1 > v2 {
stack.PushInt(1)
} else if v1 == v2 {
stack.PushInt(0)
} else {
stack.PushInt(-1)
}
}代码解释
Execute()方法把栈顶的两个long变量弹出,进行比较,然后把比较结果(int型0、1或-1)推入栈顶,代码如下:

fcmp<op>和dcmp<op>指令
fcmpg和fcmpl指令用于比较float变量。
整体代码
在instructions\comparisons目录下创建fcmp.go文件,在其中定义fcmpg和fcmpl指令,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Compare float
type FCMPG struct{ base.NoOperandsInstruction }
func (self *FCMPG) Execute(frame *rtda.Frame) {
_fcmp(frame, true)
}
type FCMPL struct{ base.NoOperandsInstruction }
func (self *FCMPL) Execute(frame *rtda.Frame) {
_fcmp(frame, false)
}
func _fcmp(frame *rtda.Frame, gFlag bool) {
stack := frame.OperandStack()
v2 := stack.PopFloat()
v1 := stack.PopFloat()
if v1 > v2 {
stack.PushInt(1)
} else if v1 == v2 {
stack.PushInt(0)
} else if v1 < v2 {
stack.PushInt(-1)
} else if gFlag {
stack.PushInt(1)
} else {
stack.PushInt(-1)
}
}
在instructions\comparisons目录下创建dcmp.go文件,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Compare double
type DCMPG struct{ base.NoOperandsInstruction }
func (self *DCMPG) Execute(frame *rtda.Frame) {
_dcmp(frame, true)
}
type DCMPL struct{ base.NoOperandsInstruction }
func (self *DCMPL) Execute(frame *rtda.Frame) {
_dcmp(frame, false)
}
func _dcmp(frame *rtda.Frame, gFlag bool) {
stack := frame.OperandStack()
v2 := stack.PopDouble()
v1 := stack.PopDouble()
if v1 > v2 {
stack.PushInt(1)
} else if v1 == v2 {
stack.PushInt(0)
} else if v1 < v2 {
stack.PushInt(-1)
} else if gFlag {
stack.PushInt(1)
} else {
stack.PushInt(-1)
}
}
代码解释
dcmpg和dcmpl指令用来比较double变量,代码基本上与fcmpg和fcmpl指令完全一样,这里就不详细介绍了。
就介绍一下fcmpg和fcmpl这两条指令:
和lcmp指令很像,但是除了比较的变量类型不同以外,还有一个重要的区别。由于浮点数计算有可能产生NaN(Not a Number)值,所以比较两个浮点数时,除了大于、等于、小于之外,还有第4种结果:无法比较。fcmpg和fcmpl指令的区别就在于对第4种结果的定义。编写一个函数来统一比较float变量,代码如下:

fcmpg和fcmpl指令的Execute()方法只是简单地调用_fcmp()函数而已,代码如下:

也就是说,当两个float变量中至少有一个是NaN时,用fcmpg指令比较的结果是1,而用fcmpl指令比较的结果是-1。
if<cond>指令
整体代码
在instructions\comparisons目录下创建ifcond.go文件,在其中定义6条if<cond>指令,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Branch if int comparison with zero succeeds
type IFEQ struct{ base.BranchInstruction }
func (self *IFEQ) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val == 0 {
base.Branch(frame, self.Offset)
}
}
type IFNE struct{ base.BranchInstruction }
func (self *IFNE) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val != 0 {
base.Branch(frame, self.Offset)
}
}
type IFLT struct{ base.BranchInstruction }
func (self *IFLT) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val < 0 {
base.Branch(frame, self.Offset)
}
}
type IFLE struct{ base.BranchInstruction }
func (self *IFLE) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val <= 0 {
base.Branch(frame, self.Offset)
}
}
type IFGT struct{ base.BranchInstruction }
func (self *IFGT) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val > 0 {
base.Branch(frame, self.Offset)
}
}
type IFGE struct{ base.BranchInstruction }
func (self *IFGE) Execute(frame *rtda.Frame) {
val := frame.OperandStack().PopInt()
if val >= 0 {
base.Branch(frame, self.Offset)
}
}
真正的跳转逻辑在Branch()函数中。
因为这个函数在很多指令中都会用到,所以把它定义在ch05\instructions\base\branch_logic.go
文件中,代码如下:
package base
import "jvmgo/ch05/rtda"
func Branch(frame *rtda.Frame, offset int) {
pc := frame.Thread().PC()
nextPC := pc + offset
frame.SetNextPC(nextPC)
}
代码解释
指令助记符加上”if_”后,以字符”i”开头的指令针对int型整数操作(也包括short和byte类型),以字符”a”开头的指令表示对象引用的比较。

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。
而if<cond>指令把操作数栈顶的int变量弹出,然后跟0进行比较,满足条件则跳转。
以ifeq指令为例,其Execute()方法如下:

而Frame结构体的SetNextPC()方法将在5.12小节介绍。
if_icmp<cond>指令
整体代码
在instructions\comparisons目录下创建if_icmp.go文件,在其中定义6条if_icmp指令,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Branch if int comparison succeeds
type IF_ICMPEQ struct{ base.BranchInstruction }
func (self *IF_ICMPEQ) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 == val2 {
base.Branch(frame, self.Offset)
}
}
type IF_ICMPNE struct{ base.BranchInstruction }
func (self *IF_ICMPNE) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 != val2 {
base.Branch(frame, self.Offset)
}
}
type IF_ICMPLT struct{ base.BranchInstruction }
func (self *IF_ICMPLT) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 < val2 {
base.Branch(frame, self.Offset)
}
}
type IF_ICMPLE struct{ base.BranchInstruction }
func (self *IF_ICMPLE) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 <= val2 {
base.Branch(frame, self.Offset)
}
}
type IF_ICMPGT struct{ base.BranchInstruction }
func (self *IF_ICMPGT) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 > val2 {
base.Branch(frame, self.Offset)
}
}
type IF_ICMPGE struct{ base.BranchInstruction }
func (self *IF_ICMPGE) Execute(frame *rtda.Frame) {
if val1, val2 := _icmpPop(frame); val1 >= val2 {
base.Branch(frame, self.Offset)
}
}
func _icmpPop(frame *rtda.Frame) (val1, val2 int32) {
stack := frame.OperandStack()
val2 = stack.PopInt()
val1 = stack.PopInt()
return
}
代码解释

if_icmp<cond>指令把栈顶的两个int变量弹出,然后进行比较,满足条件则跳转。跳转条件和if<cond>指令类似。以if_icmpne指令为例,其Execute()方法如下:

if_acmp<cond>指令
整体代码
在instructions\comparisons目录下创建if_acmp.go文件,在其中定义两条if_acmp<cond>指令,代码如下:
package comparisons
import (
"jvmgo/ch05/instructions/base"
"jvmgo/ch05/rtda"
)
// Branch if reference comparison succeeds
type IF_ACMPEQ struct{ base.BranchInstruction }
func (self *IF_ACMPEQ) Execute(frame *rtda.Frame) {
if _acmp(frame) {
base.Branch(frame, self.Offset)
}
}
type IF_ACMPNE struct{ base.BranchInstruction }
func (self *IF_ACMPNE) Execute(frame *rtda.Frame) {
if !_acmp(frame) {
base.Branch(frame, self.Offset)
}
}
func _acmp(frame *rtda.Frame) bool {
stack := frame.OperandStack()
ref2 := stack.PopRef()
ref1 := stack.PopRef()
return ref1 == ref2 // todo
}
代码解释
if_acmpeq和if_acmpne指令把栈顶的两个引用弹出,根据引用是否相同进行跳转。以if_acmpeq指令为例,其Execute()方法如下:

3.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


