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语句的替代。
 
 
 
 
 
 
 
 
 
 
 
posted @ 2020-07-16 16:09  像风一样无影无起  阅读(502)  评论(0)    收藏  举报