这是由一个“无聊”的问题引发的故事:方法ipp和ppi分别会打印什么结果?
public class Opcode { public static void main(String[] args) { System.out.println("hello wang ni ma"); } public void ipp(){ int i = 0; i = i++; System.out.println(i); } public void ppi(){ int i = 0; i = ++i; System.out.println(i); } }
当然了,把两个方法放在一起,凭借些许的逻辑思维分析,可以很快给出答案: 0 1
那JVM为什么会执行出这样的结果呢,本文将结合 字节码 和 虚拟机栈 做出解释。
javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。通过它,我们可以对照源代码和字节码,从而了解很多编译器内部的工作。
Java 程序编译之后就变成了一条条字节码指令,其形式类似汇编,但和汇编有不同之处:
由于操作数栈是内存空间,所以字节码指令不必担心不同机器上寄存器以及机器指令的差别,从而做到了 平台无关 。
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。
列举本文用到的基本指令:
将一个局部变量加载到操作栈: iload_<n>
将一个数值从操作数栈存储到局部变量表: istore_<n>
将一个常量加载到操作数栈: iconst_<i>
局部变量自增指令: iinc
对于大部分为与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:
我们用javac编译上面的Opcode.java,然后“javap -c”查看字节码:
javap命令加入“-v”可以看到更详细的信息(常量池) :
在看图之前我们先了解几个概念:
把常量“0”加载到操作数栈,指令“iconst_0”中的“0”代表int常量“0”
操作数栈:[0]
局部变量表:[this]
“i = i++;”进行了四次操作:
1 “istore_1”将操作数栈中栈顶的int压入局部变量表“1”的位置
操作数栈:[]
局部变量表:[this, 0]
2 “iload_1”将局部变量表“1”处的int加载到操作数栈
操作数栈:[0]
局部变量表:[this, 0]
3 “iinc 1, 1”将局部变量表“1”处的int做自增运算,结果自动入栈
操作数栈:[0]
局部变量表:[this, 1]
4 “istore_1”将操作数栈“1”处的int压入局部变量表
操作数栈:[]
局部变量表:[this, 0]
“System.out.println(i);”进行了三次操作:
1 “getstatic #2”指向常量池中的第2个位置,载入“System.out”域
2 “iload_1”将局部变量表“1”处的int加载到操作数栈
操作数栈:[0]
局部变量表:[this, 0]
3 “invokevirtul #5”指向常量池中的第5个位置,”调用实例方法“println”打印操作数栈的数值“0”
至此,ipp()方法的分析完成了,理解之后,反观ppi()方法的字节码信息,有一处不同:
相当于“i = i++”操作是先加载了局部变量表中的“0”到操作数栈,然后在局部变量表中做自增运算;而“i = ++i”是先在局部变量表中做自增运算,此时的值已经变成“1”,然后再把局部变量表中的“1”加载到操作数栈。这也就印证了坊间流传的“i++是先赋值后运算,++i是先运算后赋值”这一说法。
思考下面这段代码会输出什么:
int i = 0; System.out.println(i+++i);
以上内容是本人对JVM阶段性学习的总结,过程是以“《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》——方志明著”为主导,网络诸多老师的文章为辅助。希望可以做到见微知著,同时本文内容如有错误或引起歧义,欢迎读者留言予以斧正。