从JVM字节码指令来分析i++和++i

1. 前言

当我们遇到像i++或者--i这种时可能分不清有啥区别或者说不知道为什么是这样的结果,比如下面

image-20210710185326824

最后结果是4+4*2+3还是3+5*2+5还是其他情况呢,++*的优先级谁更高

接下来我们从字节码指令层面去看看到底怎么执行的

2. 栈帧(Stack Frame)

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

本文需要了解的是局部变量表和操作数栈

  • 局部变量表:局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,存储在数组中
  • 操作数栈:当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。

image-20210710192227902

在活动线程中只有位于栈顶的栈帧的有效的。

3. JVM字节码指令

使用javap -verbose xxx.class反编译class生成字节码,可以查看常量池、方法、接口、属性等信息

image-20210710220408090

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1 #操作数栈最大深度3,局部变量2个,参数数量1个
0: iconst_3 #将常量(int)3压入操作数栈顶
1: istore_1 #将栈顶的3弹出,保存在局部变量1中 此时i=3
2: iload_1 #将局部变量1的值压入栈顶
3: iinc 1, 1 #局部变量2自增1 此时i=4
6: iinc 1, 1 #局部变量2自增1 此时i=5
9: iload_1 #将局部变量1的值压入栈顶 此时栈顶两个值:5 3
10: iconst_2 #将常量(int)2压入操作数栈顶 此时栈顶三个值:2 5 3
11: imul #弹出两个栈顶值相乘,最后结果压入栈顶 此时栈顶2个值:10 3
12: iadd #弹出两个栈顶值相加,最后结果压入栈顶 此时栈顶1个值:13
13: iload_1 #将局部变量1的值压入栈顶 此时栈顶2个值:5 13
14: iinc 1, -1 #局部变量1自减1 此时i=4
17: iadd #弹出两个栈顶值相加,最后结果压入栈顶 此时栈顶1个值:18
18: istore_1 #将栈顶的18弹出,保存在局部变量1中 此时i=18
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
22: iload_1 #将局部变量1的值压入栈顶
23: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
26: return
LineNumberTable: #行数对应关系 代码/指令
line 9: 0
line 11: 2
line 12: 19
line 13: 26
}

最后就是获取流进行调用实例方法进行输出了,所以最后结果就是2*5 +3+5=18

3.1 图解

image-20210711150556139

4. 避雷

一开始使用的是IDEA的class反编译生成的jvm指令,最后发现结果不太对,找了很久原因才发现是因为IDEA为了方便我们查看class,反编译了class,但是这个class有点不太对,最后导致字节码不太对,使用java自带的命令就没问题,或者使用`jclasslib Bytecode Viewer`这个插件来进行查看
剩下的一个疑问是class既然有问题,最后运行结果为什么没错

局部变量表


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!