Java虚拟机字节码初探:三目运算符与ifelse在代码运行时是否有性能上的区别?
对于Java来说其编写好的文件为.java文件,经过编译产生.class文件,对于Java虚拟机来说,class文件中存放着Java代码运行的指令。
新版本的JDK拥有很多语法上的新特性,在我们从老版本的JDK升级到新版本的JDK时,总是不适应于新的语法编写,新的语法在编译后总是比旧的语法性能高吗?
对于性能的判断总是要基于实际情况进行分析,我们需要对照代码及数据分析性能的好坏。
在编写好代码后,怎么能看到Java虚拟机实际运行的指令呢?
这就需要两个知识点:
1、javap命令:
用来查看编写后的代码被Java编译器“翻译”为什么样的机器(JVM虚拟机)语言。
2、Java虚拟机指令集的意义:
编译后的语言对于我们开发人员来说并不容易看懂,需要对照文档中的每个指令的意义进行翻译。
javap 命令:
javap是jdk自带的一个class字节码反编译器,在命令行中输入javap -help可以看到相关的所有命令:
用法: javap <options> <classes> 其中, 可能的选项包括: -help --help -? 输出此用法消息 -version 版本信息 -v -verbose 输出附加信息 -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的 系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示最终常量 -classpath <path> 指定查找用户类文件的位置 -cp <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置
本次我们使用到的短命令是-c,对代码进行反汇编
两个类:
public class Test{ public static void main(String[] args){ int i = 9 ; int j = 0 ; if( i > 0 ){ j = 1; }else{ j = -1; } } }
public class Test2{ public static void main(String[] args){ int i = 9 ; int j = 0 ; j = i > 0 ? 1 : -1; } }
Test.class
Compiled from "Test.java" public class Test { public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 9 2: istore_1 3: iconst_0 4: istore_2 5: iload_1 6: ifle 14 9: iconst_1 10: istore_2 11: goto 16 14: iconst_m1 15: istore_2 16: return }
Test2.class
Compiled from "Test2.java" public class Test2 { public Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 9 2: istore_1 3: iconst_0 4: istore_2 5: iload_1 6: ifle 13 9: iconst_1 10: goto 14 13: iconst_m1 14: istore_2 15: return }
如果想看得懂这些字符,就必须得知道每一个指令的意义,这就涉及到JDK官方提供的文档,通过文档的对比,我们可以看到这些指令的意义。
JDK8 的JVM 指令集 官方文档地址:
可以查询到:
|
指令
|
操作
|
格式 |
描述
|
备注
|
| bipush |
压入字节
|
bipush = 16(0x10) |
将直接值字节扩展为带符号的int值,该值被立即压入操作栈
|
|
|
istore
|
将int存入临时变量
|
istore = 54 (0x36)
|
索引是一个无符号字节,必须是当前帧的局部变量数组的索引。 操作数堆栈顶部的值必须是int类型。 它从操作数堆栈中弹出,并且索引处的局部变量的值设置为value。
|
istore操作码可与Wide指令结合使用,以使用两字节无符号索引访问局部变量。
|
|
iconst
|
int常量赋值
|
iconst_m1 = 2 (0x2)
|
将int常量<i>压入操作数堆栈。
|
对于每个<i>值,此指令族的每个指令均等效于bipush <i>,但操作数<i>是隐式的。
|
|
iload
|
从常量中加载int值
|
iload = 21 (0x15)
|
索引是一个无符号字节,必须是当前帧的局部变量数组的索引。 索引处的局部变量必须包含一个int。 索引处的局部变量的值被压入操作数堆栈。
|
iload操作码可与Wide指令结合使用,以使用两字节无符号索引访问局部变量。
|
|
if<cond>
|
如果int比较为零成功,则分支
|
ifeq = 153 (0x99)
ifne = 154 (0x9a)
iflt = 155 (0x9b)
ifge = 156 (0x9c)
ifgt = 157 (0x9d)
ifle = 158 (0x9e
|
该值必须是int类型。 它从操作数堆栈中弹出,并与零进行比较。 所有比较均已签名。 比较结果如下:
当且仅当value = 0时ifeq成功
当且仅当值≠0时ifne成功
如果且仅当值<0时,iflt成功
当且仅当值≤0时,ifle成功
当且仅当值> 0时ifgt成功
当且仅当值≥0时,ifge成功
如果比较成功,则使用无符号的branchbyte1和branchbyte2构造一个带符号的16位偏移量,该偏移量的计算公式为(branchbyte1 << 8)| | |。 branchbyte2。 然后,执行从该if <cond>指令的操作码地址开始的那个偏移量处进行。 目标地址必须是包含if <cond>指令的方法中指令的操作码的地址。
否则,执行在此if <cond>指令之后的指令地址处进行。
|
|
通过这些指令的解释就可以容易的看懂这些指令的意义:
首先将直接值9压入操作栈,创建临时变量istore_1 ,将9的直接值压入临时变量istore_1中;
创建临时变量istore_2,将常量加载,压入操作数堆栈,并执行ifle操作……
在这里也可以看出编译器进行了指令重排,并且优化了部分无用的代码。
回归问题:
三目运算符与ifelse在代码运行时是否有性能上的区别?
其答案也是非常清楚了,对于三目运算符来说,其省去了一步赋值操作,但是对于实际开发来说三目运算符的可读性相对不如ifelse代码,所以在常见的编码中,三目运算符更倾向于简单的ifelse语句的替代。

浙公网安备 33010602011771号