引言:本文通过一个问题来引出字节码指令、运行时数据区栈帧的简单结构,从JVM的角度来回答这个问题,笔者只是做一个学习总结,如有不解之处可以评论区提问,若有错漏之处欢迎批评指正!
1.先从代码的角度分析
@Test
public void test1(){
int i = 10;
i++;
//++i;
System.out.println(i);// 结果都是11
}
毫无疑问这里i的值都是11
@Test
public void test2(){
int i = 10;
i = i++;
//i = ++i;
System.out.println(i);// 结果分别是i++:10,++i:11
}
第二段代码就有点疑问了,初学Java时,有一句话是这么总结的,++在后则先复制再加+1,++在前则反之。
按照这个总结,那先给i赋值后再+1,结果难道不应该是11?其实不然,还有一个规则就是i++并没有使用过,所以导致最终并不能保存下来(暂时只能这么理解i++)
@Test
public void test3(){
int k = 10;
k = k + (k++) + (++k);
System.out.println(k);//32
}
第三段代码比较好分析,k=10+10+12,即结果为32
目前的问题是,我们用的是一个总结即“++在前,先+1再运算;++在后,先运算再+1”来进行分析代码,然后对于同一个变量为何自加后没有改变其自身的值,这是为什么呢?
2.字节码文件与字节码指令
众所周知,我们编写好的java文件,经过编译器的编译生成class文件即字节码文件,它是一种二进制文件,其中包含字节码指令,这些指令按照一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。
JVM通过识别并执行这些指令来实现我们的功能。我们可以通过工具进行反编译来将二进制的文件变成可视化的操作码和操作数组成的一行行指令,这样我们就可以阅读了。
本文使用的是IDEA插件jclasslib
。
通过编译与反编译可以得到以下指令:
test1方法:
test2方法:
i = i++的情况:
i = ++i的情况:
非常明显能看到对应的代码中有两行指令顺序不一致!
test3方法:
这样我们就得到了每个方法的字节码指令,但是具体的指令代表什么意思呢?又是怎么执行呢?本文尽量简化,只用到局部变量表和操作数栈的结构来解释。
3.运行
这里尽量简化了结构:
test1方法运行
bipush
:将单字节的常量值x(-128~127)推送至栈顶(入栈)
istore_x
:将栈顶int型数值存入本地变量表索引x位置(出栈)
iinc x by y
:将局部变量表x位置的值增加指定值y
test2方法运行
关注i = i++;
三条指令,③④⑤此类编号表示上面图示中方法字节码指令最左侧的行号
③iload_x
:将局部变量表位置x的值加载到栈顶(入栈)
④:将局部变量表位置1的值增加1
⑤:将栈顶的int值保存到局部变量表位置1的位置(出栈)(覆盖原本的值)
如果这里的代码是i = ++i
,那么①和②的顺序调换一下即可。
所以由此可以得到,++i
与i++
在JVM层面的区别!
++
操作的字节码指令为iinc x by y
JVM在执行字节码指令时,如果是变量名在前,则先将对应的局部变量的值入栈,再进行innc
操作;反之则先进行innc
操作,再入栈!
test3方法运行
关注代码k = k + (k++) + (++k);
,③④⑤此类编号表示上面图示中方法字节码指令最左侧的行号
③:将局部变量表位置1的值压入栈顶
④:将局部变量表位置1的值压入栈顶
⑤:将局部变量表位置1的值增加1
⑥iadd
:将栈顶两个int数值相加并压入栈顶
⑦:将局部变量表位置1的值增加1
⑧:将局部变量表位置1的值压入栈顶
⑨:将栈顶两个int数值相加并压入栈顶
⑩:将栈顶的int型数值出栈保存到局部变量表位置1中
4.总结
JVM层面讨论的就是字节码指令的执行。不同的代码编译后产生的字节码指令以及不同的顺序,都会影响最后的执行结果。这中还包含了运行时数据区:虚拟机栈中栈帧的局部变量表和操作数栈的相关知识。学习Java和有必要去了解底层,知其然而不知其所以然是不可取的。