JVM对算术运算做了什么??
java可以进行数字的加减乘除,但是JVM的运算步骤是什么样子呢?从一个神奇的式子入手,研究下JVM到底做了什么?
先看下图:
简直亮瞎了我的钛合金狗眼,刚看到这个式子的时候,我百思不得其解,最后在群友们和一个同事的解释下,才明白,这和JVM对算术运算的操作原理有关系,其实就是一个入栈和出栈的操作过程。
如果不明白底层的栈操作,乍一看这个式子,简直就像是突破了自己的常识,就算是运算符的优先级,那也是这样:
- 第一步,计算 (b = a),那么 b此时已经是100
- 第二步,计算 (b = a) * 0 = 100 * 0 = 0
- 第三步,计算 b + (b = a) * 0 = 100 + 0 = 100
- 第四步,把 b = 100 赋给 a ,那么 a 也是 100
正常来说,我脑补出来的过程就是这样的顺序,但是结果很打脸, b 是 100 我 还能理解,但是 a 不应该也是 100 么,漏掉了什么呢?难道还同时存在两个 b ? 第一个 b 和 第 二 b 的值其实是不一样的?怎么可能呢?在第一步的时候不是已经把 b 的值 给改掉了吗?
肯定是我漏掉了什么,上面的运行步骤肯定是漏了什么,是哪一部分呢,是入栈和出栈的真实过程,程序并不是按我主观想象来运行的,而是依赖于作者的底层设计。只能是同时存在两个b , 第一个 b 和 第二个 b 其实是不一样的,互不影响的,才能解释程序运行的结果。但是,具体是怎么样的一个过程呢?
在说之前,先补一补 栈 这个东西。
栈
虚拟机栈:表示Java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的。
不过我们这里要说的栈还不是虚拟机栈,而是虚拟机栈里,一个栈帧的操作数栈。因为,我这里只是演示一个方法而已,这一个方法其实就是一个栈帧。
栈 是 后入先出(Last In First Out)LIFO栈。
借周老师一张图:
一个栈帧主要由四部分组成:
- 局部变量表
- 操作栈(也叫操作数栈)
- 动态连接
- 返回地址信息
具体有机会再啰嗦,还是让我们回归正题,分析一下这段代码:
打开CMD: 执行java命令
我截取主要的字节码,来捋一下程序的真实运行情况:
Code:
stack=3, locals=3, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: iload_2
8: iload_1
9: dup
10: istore_2
11: iconst_0
12: imul
13: iadd
14: istore_1
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: new #3 // class java/lang/StringBuilder
21: dup
22: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
25: ldc #5 // String a =
27: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: iload_1
31: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: ldc #8 // String ,b =
36: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: iload_2
40: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
43: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
46: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
49: return
在这里主要用到的就是 操作数栈和局部变量表 数值之间的来回倒腾。
从操作数栈 到 局部变量表 是出栈(也叫弹栈)的过程,一般用单词 store 相关的指令。
从局部变量表 到 操作数栈 是入栈(也叫压栈)的过程 ,意从局部变量表到操作数栈,数值是复制一份过去,并不改变局部变量表里的值,一般用 单词 load 相关的指令。
再次说一下栈这个东西,类比现实中的事物,就和 汉诺塔 一样,就下图这玩意:
你先放入的,只能最后才能拿出来,因为它只有一个出口。
操作数栈,这个命名其实很贴切,就是为了操作,就是为了操作数的一个后入先出的东西。
继续看字节码:
这块是我这个方法最主要的部分。
stack=3 , 表示 这个栈帧的 操作数栈的深度,最多是3,就像汉诺塔的层数一样,最多是3。刚开始里面是3个空。运算的时候会来回倒腾它。
locals=3,表示 这个栈帧的 局部变量表的局部变量空间是3个Slot ,Slot是局部变量表的最小单元, 一个可以存储32位大小的数据。直白来说就是现在它能存3个变量值到这里面。一般是从索引 1开始取数,不是0没有,而是被关键字“this”占用了,具体的再找时间啰嗦。
画个图,感受一下:
1.初始状态
stack=3, locals=3, args_size=1
- 执行指令 bipush ,作用是将单字节的整数常量值(-128 - 127 )推入操作数栈顶,跟随有一个参数,指明推送的常量值,这里是100,那就是这样了呗:
- 执行指令 istore_1 ,作用是 将操作数栈顶的整型值出栈 并存放到 局部变量表的 索引为1 的Slot 中。如图:
4.执行指令 sipush , 作用和 bipush 差不多,就是 把指定的参数值(这里是200) 入操作数栈,如图:
5.执行指令 istore_2 , 作用和 istore_1 一样 , 就是 把操作数栈栈顶的数值 出栈,并存放到 局部变量表 的索引为2 的Slot 中。如图:
6.接着两条 load 指令,iload_2 , iload_1 这两个的作用是 把Slot2 和 Slot1 的数组复制一份 放入 到操作数栈中。那就变成如图:
OK ,看到了吗,现在 a 和 b 其实都有了两份在内存里,一个在操作数栈里,一个在局部变量表里。好吧,继续看我们的指令,看到最后会是什么结果。
7.执行指令 dup , 这个指令什么意思呢?意思是 “复制栈顶一个或者两个数值并将复制值重新压入栈顶” ,那就是如图。复制不了两个,只能复制一个,深度最大才是3嘛。而且 dup 就是复制一个。
8.执行指令 istore_2 , 这个指令,上面讲过,把 栈顶数据 出栈,存入 Slot2 ,那就是如图:
看,现在 b 的这个位置变成 100了,好像有点意思了,那怎么把 a 变成 200 呢,继续往下看。
9.执行指令 iconst_0 ,这个指令的意思是 取 一个 -1 ~ 5 之间的常量,并压入操作数栈,取谁呢,给的参数是谁,就取谁,取 0 。那就变成这样:
- 执行指令 imul ,这个指令的意思是 乘法指令,是对两个操作数栈上的值进行乘法运算,并把结果重新压入栈中。那就变成这样:(0 * 100 = 0)
11.执行指令 iadd ,这个指令的意思是 加法指令 ,是对两个操作数栈上的值进行 加法运算,并把结果重新压入栈中。那就变成这样:(0 + 200 = 200)
12.执行指令 istore_1 ,这个指令上面讲过了,把栈顶数据出栈,并存放到Slot1中。如图:
到这一步,基本上,已经可以看出来a 是怎么变成 200 的了。那我们继续往下再看看,还有啥。
13.再看最后一张图吧:
大意是 ,把 Slot1 的值给 a , 把Slot 的值给 b, 输出之后,祭出来我们的第一张图:
【完】
看似很简单的一个表达式,结果折磨了我两天,才稍微捋出来个大概,真是太菜了。其实问题的答案都已经写在书里了,有兴趣的小伙伴可以翻一翻 《深入理解Java虚拟机》 的第六章 和 第八章,肯定会有所收获。感谢群里积极回答我提问的 @万籁,@小白,@明哥的舔狗,明哥是个小可耐 等大佬。还有我牛逼的同事给我的一番解惑操作。
后面我会再多找一些有意思的表达式,继续深入 理解一下Java虚拟机的运行过程,有兴趣的小伙伴可以关注我,一块讨论,一块嗨皮~
若有错漏,欢迎指正!