【JVM】字节码指令简介(二)
类型转换指令
类型转换指令值得是两种虚拟机数值类型之间的转换,不是Java中数值类型的转换,一般是用在用户代码中的显示类型转换,或者是用来解决Java虚拟机字节码指令不完整是。类型转换指令可以分为两种,包括宽化类型转换(widening numeric conversion) 和窄化类型转换(narrowing numeric conversion) 。
宽化类型转换
宽化类型转换指的是数值类型从小范围的数值类型向大范围的数值类型之间的转换,宽化类型转换是不会因为超过目标类型最大值而丢失信息,宽化类型转换又包括以下三种:
- int类型转换为long、float或者double类型
- long类型转换为float、double类型
- float类型转换为double类型
宽化类型转换指令包括: i2l, i2f, i2d, l2f, l2d, f2d
指令中的2代表的是to,从操作码的类型很容易就知道每个指令的含义
long占用2个slot,float占用1个slot,为什么long转float是宽化类型转换?
虽然long类型占用8字节,float占用4字节,float的数据范围是-3.410^38 ~ 3.410^38,而long类型的数据范围是-2^31 ~ 2^31-1,long的数据范围小于float数据范围,虽然有可能会导致精度降低,但是不会因为超过float数据类型而导致信息丢失。
窄化类型转换
窄化类型转换指的是可能会因为超过目标类型最大值导致转换结果具备不同正负号、不同数量级等严重问题的转换指令。窄化类型转换包括如下四种
- int类型到byte、short或者char类型
- long类型到int类型
- float类型到int或者long类型
- double类型到int、long或者float类型
窄化类型转换指令包括: i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f
这两中转换类型我们通过代码来演示一下
public void dataTypeChange() {
int i=100;
byte x = (byte) i;
long y = (long) i;
}
编译得到字节码如下
0 bipush 100
2 istore_1
3 iload_1
4 i2b // 窄化类型转换 int -> byte
5 istore_2
6 iload_1
7 i2l // 宽化类型转换 int -> long
8 lstore_3
9 return
对象创建与操作指令
对象创建和操作指令主要创建/管理的是对象或者数组,这一小节的指令通过根据名称还是比较好理解的。
指令 | 作用 |
---|---|
new | 创建类实例 |
newarray, anewarray, multianwarray | 创建新数组 |
getfield, putfield, getstatic, putstatic | 访问类的域和类实例域 |
arraylength | 获取数组长度 |
instanceof, checkcast | 检相类实例或数组属性 |
通过如下代码来演示对象创建和操作指令
public void typeChange(Parent parent) {
Child child = null;
if (parent instanceof Child) {
child = (Child) parent;
}else {
child = new Child();
}
}
编译完字节码如下
0 aconst_null
1 astore_2
2 aload_1
3 instanceof #2 <jvm/parent/Child> // 判断是否变量是否是Child的实例
6 ifeq 17 (+11)
9 aload_1
10 checkcast #2 <jvm/parent/Child> // 强制转换之前,字节码加上checkcast指令
13 astore_2
14 goto 25 (+11)
17 new #2 <jvm/parent/Child> // 创建child对象
20 dup
21 invokespecial #3 <jvm/parent/Child.<init> : ()V>
24 astore_2
25 return
操作数栈管理指令
操作数栈管理指令是用于直接操作操作数栈的指令,具体操作数管理指令见下面表格
指令 | 作用 |
---|---|
pop | 将操作数栈栈顶元素弹栈,并丢弃 |
pop2 | 将操作数栈栈顶1个或2个的值弹栈,并丢弃 |
dup | 复制操作数栈栈顶元素,并压入栈 |
dup2 | 复制操作数栈栈顶1个或2个的值,并压入栈 |
dup_x1 | 复制操作数堆栈上的顶值并插入栈顶往下数两个值之下 |
dup_x2 | 复制操作数栈顶部操的值并向下插入2个或3个值之下 |
dup2_x1 | 复制操作数栈顶1个或2个值并向下插入2个或3个值之下 |
dup2_x2 | 复制操作数栈顶1个或2个值并向下插入2个,3个或4个值之下 |
swap | 交换操作数栈顶2个值 |
-
pop
pop指令会将操作数栈栈顶的第1个slot弹出,并丢弃
-
pop2
作用与pop相同,都是将操作数栈栈顶元素弹出
pop2和pop有什么区别?
pop2在Java虚拟机规范中描述原文:Pop the top one or two values from the operand stack.
这句话相当难理解,直翻出来大致是:将操作数栈顶的1个或2个元素弹出
虚拟机规范中并没有说明什么时候弹出1个值,什么时候弹出2个值。
其实如果栈顶元素是64位的(long或者double类型),就需要使用pop2弹栈,如果栈顶元素是32位的则,则使用pop弹栈
我们写一个简单代码演示如下:
public static void main(String[] args) {
System.currentTimeMillis();
Integer.parseInt("1");
}
编译后字节码如下
0 invokestatic #4 <java/lang/System.currentTimeMillis : ()J>
3 pop2 // long弹栈
4 ldc #5 <1>
6 invokestatic #6 <java/lang/Integer.parseInt : (Ljava/lang/String;)I>
9 pop // int弹栈
10 return
- dup
复制操作数栈栈顶元素,并压入栈,过程如下图,整体还是比较好理解的
- dup2
dup2的作用与dup相同,有了前面pop2的经验,可以推断出如果栈顶元素是64位则使用dup2指令,如果栈顶元素是32位则使用dup指令。
我们写一个简单代码演示如下:
public void dup() {
long i=0;
long j=0;
i=j = System.currentTimeMillis();
}
我们写一个简单代码演示如下:
0 lconst_0
1 lstore_1
2 lconst_0
3 lstore_3
4 invokestatic #4 <java/lang/System.currentTimeMillis : ()J>
7 dup2 // 复制栈顶的long
8 lstore_3
9 lstore_1
10 return
- dup_x1
复制操作数堆栈上的顶值并插入栈顶往下数2个slot之下,直接理解这个描述看起来比较费劲,具体如下图所示
我们写一个简单代码演示如下:
private int id=0;
public int dup_x1() {
return ++id;
}
编译后的字节码
0 aload_0
1 dup
2 getfield #2 <jvm/opcode/DupOpCode.id : I>
5 iconst_1
6 iadd
7 dup_x1
8 putfield #2 <jvm/opcode/DupOpCode.id : I>
11 ireturn
直接看字节码指令依然比较复杂。我们通过下图可以更清楚的看到执行完iadd指令之后的操作之后每个指令执行完操作数栈的情况。首先执行完iadd指令之后,操作数栈中的元素包括数值1和this的引用。然后执行dup_x1,则会复制一个X1,然后将数值插入从栈顶往下数两个slot下面,由于数值1和this分别占用1个slot,因此会把复制出来的1插入到this之下,然后然后执行putfield消耗两个slot中的值,最后操作数栈中剩下1再执行返回。
- dup_x2
复制操作数栈顶部的值,并插入从栈顶往下数2个或3个slot之下。这句话看起来又是无法理解,但是这真的是从Java虚拟机规范原文翻译过来的(很有可能是笔者的翻译水平有限)。
《Java虚拟机规范》原文:Duplicate the top value on the operand stack and insert the duplicated value two or three values down in the operand stack.
我们写一个简单代码演示如下:
public Long dup_x2(Long[] array, int i, long j) {
return array[i]=j;
}
编译后字节码如下所示
0 aload_1
1 iload_2
2 lload_3
3 invokestatic #3 <java/lang/Long.valueOf : (J)Ljava/lang/Long;>
6 dup_x2
7 aastore
8 areturn
参考上文,还是将调用完invokestatic指令码之后的操作数栈情况画出,如下图所示。此时操作数栈从栈顶往下依次是 J的包装类引用,i以及Long[]的引用,每个元素占用1个slot,调用dup_x2指令之后,从栈顶往下数3个slot之下,也就是栈底,再执行aastore指令,消耗栈顶往下3个元素,执行完成之后操作数栈中剩余J,最后返回
- dup2_x1
主要作用是复制操作数栈顶的1个或者2个值,并且插入2~3个slot下。这个执行其实就是栈顶元素占用2个slot版本的dup_x1
《Java虚拟机规范》原文:Duplicate the top one or two operand stack values and insert two or three values down
我们写一个简单代码演示如下:
private long index=0;
public long dup2_x1() {
return ++index;
}
编译后的字节码如下所示
0 aload_0
1 dup
2 getfield #3 <jvm/opcode/DupOpCode.index : J>
5 lconst_1
6 ladd
7 dup2_x1
8 putfield #3 <jvm/opcode/DupOpCode.index : J>
11 lreturn
整个过程参考dup_x1,几乎是一样的,这里就不再单独说明了
- dup2_x2
与dup2_x1类似,这个指令就是栈顶元素占用2个slot版本的dup_x2。
《Java虚拟机规范》原文:Duplicate the top one or two operand stack values and insert two, three, or four values down
- swap指令
这个指令的作用是交换栈顶的两个元素。
转载自:https://juejin.cn/post/7199235461046861879