前言
有一天逛知乎的时候,遇到了这样的问题:面代码为什么 i 最后的结果是 8?
public static void main(String[] args) {
int i = 1;
i += i += ++i + 2.6 + i;
}
复制代码
很简单的两行代码,如果是你遇到这样的问题,你会怎样去把问题解释清楚?是利用 Java 运算符顺序将式子拆解,然后一步步运算,还是其他什么办法?
在思索一会儿之后,决定还是通过字节码指令来看看这两行代码是怎么运行的。
将两行代码拷贝到 Test.java 中,执行以下指令将 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?众所周知,==比较的是两个对象的地址,为什么两个对象的地址能一样?这里就让我们来探索一下:
源码如下:
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 源码如下:
可以看出,在 static 静态块中通过 for 循环,初始化了 cache 数组。
结语
文章可能对栈桢描述的并没有那么详细,主要还是让大家大致了解一下栈桢基本的功能作用,普及一下字节码的作用。当我们对一些代码无法理解的时候,换个角度去理解可能会豁然开朗。
评论