前言
有一天逛知乎的时候,遇到了这样的问题:
public static void main(String[] args) {
int i = 1;
i += i += ++i + 2.6 + i;
}
// 为什么i最后的结果是8?
很简单的两行代码,如果是你遇到这样的问题,你会怎样去把问题解释清楚?是利用Java运算符顺序将式子拆解,然后一步步运算,还是其他什么办法?
在思索一会儿之后,决定还是通过字节码指令来看看这两行代码是怎么运行的。
将两行代码拷贝到Test.java中,执行以下指令输出字节码:
javac Test.java
javap -c Test.class
字节码输出结果如下:
如果是之前对字节码没有了解的话,可以去搜一下字节码指令的资料,或者去《深入理解Java虚拟机》这本书去找附录b 字节码指令表。
接下来翻译一下字节码:
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 将1放入操作数栈顶
1: istore_1 // 将操作数栈顶的i出栈并存放到局部变量表中slot中
2: iload_1 // 从slot中取出i并放入操作数栈顶,此时栈内容为1
3: iload_1 // 从slot取出i再次放入操作数栈顶,此时栈内容为1 1
4: i2d // 将操作数栈顶i的int转换为double类型,此时栈内容为1.0 1
5: iinc // ++i自增,此时slot中的i的值为2,记住,是2
8: iload_1 // 从slot取出i放入栈顶,此时栈内容为2 1.0 1
9: i2d // 将栈顶的int类型转换为double类型
10: ldc2_w // 将2.6放入栈顶,此时栈内容为2.6 2.0 1.0 1
13: dadd // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 4.6 1.0 1
14: iload_1 // 将slot中的i放入栈顶,此时栈内容为 2 4.6 1.0 1
15: i2d // 将栈顶的int类型转换为double类型,此时栈内容 2.0 4.6 1.0 1
16: dadd // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 6.6 1.0 1
17: dadd // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 7.6 1
18: d2i // 将栈顶的double转换为int类型7.6变成7,此时栈内容为7 1
19: dup // 复制栈顶数值并压栈,此时栈内容为 7 7 1
20: istore_1 // 将i= i + (++i + 2.6 + i)的结果,i的值即7放入slot中,并出栈,此时栈内容7 1
21: iadd // 将栈顶两个int相加,此时栈内容为8
22: istore_1 // i = i + (i + (++i + 2.6 + i))结果,即i的值即8放入slot,并出栈
23: return // 返回8
上面的字节码注释就是我的答案,一步一步的将运算步骤进行了拆解。
栈桢
上面提到的局部变量表和slot是什么?
这里就不得不提栈桢了。当我们执行一个方法的时候,虚拟机就会在线程私有的虚拟机栈栈顶创建一个栈桢来对应此方法。所以栈桢是方法调用和执行时的数据结构,包括局部变量表、操作数栈、动态连接等。一个方法从开始调用到执行完成,对应了一个栈桢在虚拟机栈中入栈和出栈的过程。
局部变量表
局部变量表是用于存放方法参数和方法局部变量的空间,里面由一个个Slot组成。代码在编译成字节码文件的时候,就可以确定局部变量表的大小。除了64位的long和double类型占用2个slot外,其他的数据类型占用1个slot。
操作数栈
在方法执行过程中,通过各种字节码指令往操作数栈中写入和读取数据,即入栈和出栈。数据的运算基于操作栈进行,例如iadd可以将栈顶的两个int类型进行加法运算。
动态连接
每个栈桢都会包含一个指向运行时常量池中该栈桢对应方法的符号引用,持有这个引用是为了支持方法调用过程的动态连接。将符号引用在运行期解析成直接引用的过程,叫做动态连接。
方法返回地址
方法会在以下两种情况进行退出:当遇到方法返回字节码指令时,根据方法逻辑决定是否会有返回值返回给调用者,然后正常退出方法;当遇到异常时,并且没有使用try来捕获异常,导致代码异常退出。不论怎么样退出,都要返回到调用方法时的位置,栈桢中会保存方法返回时的一些信息,来恢复上层方法的执行状态。
扩展应用
最近网上比较流行的一个问题,为什么Integet类型的100 == 100返回true,200 == 200返回false?众所周知,==比较的是两个对象的地址,为什么两个对象的地址能一样的,有一点amazing。那就让我们来探索一下:
源码如下:
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b);
System.out.println(c == d);
}
输出结果:
字节码如下:
public static void main(java.lang.String[]);
Code:
0: bipush 100
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: bipush 100
8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
11: astore_2
12: sipush 200
15: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: astore_3
19: sipush 200
22: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
25: astore 4
27: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: aload_2
32: if_acmpne 39
35: iconst_1
36: goto 40
39: iconst_0
40: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
43: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_3
47: aload 4
49: if_acmpne 56
52: iconst_1
53: goto 57
56: iconst_0
57: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
60: return
从字节码中可以看到a、b、c、d赋值的时候都是通过invokestatic字节码指令调用了Integer.valueOf()方法。但是不同的是,在给a、b赋值时候字节码指令是bipush,是将单字节的整型常量值(-128 - 127)压入操作数栈顶;给c、d赋值时候字节码指令是sipush,是将int类型的常量值压入操作数栈顶。为什么同样是Integer类型,一个是1个字节,一个是4个字节呢?
那我们来探索一下Integer的valueOf()方法:
这个方法调用了重载的valueOf(),代码如下:
这个IntegerCache是Integer的一个静态内部类,会对你初始化的Integer的值进行判断,当这个值在low和high之间,即-128 ~ 127,不会重新在堆中分配内存创建Integer对象,会直接从cache数组中返回一个Integer对象,所以a == b。
IntegerCache源码如下:
结语
文章可能对栈桢描述的并没有那么详细,主要还是让大家大致了解一下栈桢基本的功能作用,普及一下字节码的作用。当我们对一些代码无法理解的时候,换个角度去理解可能会豁然开朗。