JVM对算术运算做了什么??

java可以进行数字的加减乘除,但是JVM的运算步骤是什么样子呢?从一个神奇的式子入手,研究下JVM到底做了什么?

先看下图:

简直亮瞎了我的钛合金狗眼,刚看到这个式子的时候,我百思不得其解,最后在群友们和一个同事的解释下,才明白,这和JVM对算术运算的操作原理有关系,其实就是一个入栈和出栈的操作过程。

如果不明白底层的栈操作,乍一看这个式子,简直就像是突破了自己的常识,就算是运算符的优先级,那也是这样:

  1. 第一步,计算 (b = a),那么 b此时已经是100
  2. 第二步,计算 (b = a) * 0 = 100 * 0 = 0
  3. 第三步,计算 b + (b = a) * 0 = 100 + 0 = 100
  4. 第四步,把 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
  1. 执行指令 bipush ,作用是将单字节的整数常量值(-128 - 127 )推入操作数栈顶,跟随有一个参数,指明推送的常量值,这里是100,那就是这样了呗:
  1. 执行指令 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 。那就变成这样:

  1. 执行指令 imul ,这个指令的意思是 乘法指令,是对两个操作数栈上的值进行乘法运算,并把结果重新压入栈中。那就变成这样:(0 * 100 = 0)

11.执行指令 iadd ,这个指令的意思是 加法指令 ,是对两个操作数栈上的值进行 加法运算,并把结果重新压入栈中。那就变成这样:(0 + 200 = 200)

12.执行指令 istore_1 ,这个指令上面讲过了,把栈顶数据出栈,并存放到Slot1中。如图:

到这一步,基本上,已经可以看出来a 是怎么变成 200 的了。那我们继续往下再看看,还有啥。

13.再看最后一张图吧:

大意是 ,把 Slot1 的值给 a , 把Slot 的值给 b, 输出之后,祭出来我们的第一张图:

【完】


看似很简单的一个表达式,结果折磨了我两天,才稍微捋出来个大概,真是太菜了。其实问题的答案都已经写在书里了,有兴趣的小伙伴可以翻一翻 《深入理解Java虚拟机》 的第六章 和 第八章,肯定会有所收获。感谢群里积极回答我提问的 @万籁,@小白,@明哥的舔狗,明哥是个小可耐 等大佬。还有我牛逼的同事给我的一番解惑操作。

后面我会再多找一些有意思的表达式,继续深入 理解一下Java虚拟机的运行过程,有兴趣的小伙伴可以关注我,一块讨论,一块嗨皮~

若有错漏,欢迎指正!

posted @ 2020-06-20 21:20  大水煮鱼  阅读(423)  评论(0编辑  收藏  举报