JVM之JIT编译实战与思考

概述:

JIT(just in time )编译器又称即时编译器,用于在优化JAVA代码的执行速度,在入门第一讲中已经简单概述了JIT的相关内容,

这里主要通过实战对比看看JIT的速度对比(这里主要是与lua脚本做对比,原因就是LUA脚本加上LUAJIT的运行速度比较接近C).

优化流程:

阶段1-内联


内联是将较小方法的树合并或“内联”到其调用者的树中的过程。这样可以加速频繁执行的方法调用。根据当前的优化级别,使用了两种具有不同攻击级别的内联算法。在此阶段执行的优化包括:

  1. 琐碎的内联
  2. 调用图内联
  3. 消除尾递归
  4. 虚拟呼叫守卫优化


第2阶段-本地优化

 

  1. 局部优化可以一次分析和改进一小部分代码。许多本地优化实现了经典静态编译器中使用的久经考验的技术。优化包括:
  2. 本地数据流分析和优化
  3. 注册使用情况优化
  4. Java习惯用法的简化
  5. 这些技术被反复应用,尤其是在全局优化之后,这可能已经指出了更多的改进机会。
     

阶段3-控制流程优化

 

  1.    控制流优化分析方法(或方法的特定部分)内部的控制流,并重新排列代码路径以提高其效率。优化是:
  2. 代码重新排序,拆分和删除
  3. 循环减少和反转
  4. 循环跨步和循环不变代码运动
  5. 循环展开和剥离
  6. 循环版本控制和专业化
  7. 异常导向优化
  8. 开关分析


阶段4-全局优化

全局优化可一次对整个方法起作用。它们更加“昂贵”,需要大量的编译时间,但可以大大提高性能。优化是:

  1. 全局数据流分析和优化
  2. 消除部分冗余
  3. 转义分析
  4. GC和内存分配优化
  5. 同步优化
     

阶段5-本机代码生成
     本机代码生成过程因平台架构而异。通常,在编译的此阶段,将方法的树转换为机器代码指令;根据架构特征执行一些小的优化。编译后的代码被放入JVM进程空间的一部分,即代码缓存。记录方法在代码缓存中的位置,以便将来对其进行调用将调用已编译的代码。在任何给定时间,JVM进程都由JVM可执行文件和一组JIT编译的代码组成,这些代码动态链接到JVM中的字节码解释器。

 

看到这里我们可以简单的总结出JIT就是通过一系列的算法和结构把原来的代码做了一层转换,使得同样的顶层语法计算机执行的效率完全不一样,那么最终JVM字节码解析器解析的时候就会有两种选择了,本篇文章即为实战那么我们先引入一个问题,我们学这个有什么用?

答: 我们学习这个一方面可以对JVM解析字节码有更深的了解,另一方面也可以在日常开发中写出高效代码、至于实战往下看

实际上JIT是比较底层的也是默认开启的我们可以看下:

我们可以看到 mixed mode ,也就是说默认是解释编译共同运行,这里辩证一下,说JAVA是编译行语言的具体应该是只JAVA到JAVA字节码阶段,JAVA其实大部分语言都是解释运行的这就是为什么JAVA会比较慢了,那么JAVA解释的流程是在JVM内部字节码解析器解读字节码转为机器码供CPU寄存器(%eax ...)识别的过程。

 

JIT实战:

我们先来看一段JAVA代码:

     int  count = 0;
        long time1  = System.currentTimeMillis();
//        count = getCount(count);
        for (int i = 1 ; i < 1000000000 ; i ++){
            count +=i ;
        }
        long time2  = System.currentTimeMillis();
        System.err.println(time2-time1);
        return ResponseEntity.ok(count);
    }

以上进行1 到100 亿的累加,并且循环

执行结果和时间(毫秒)如下:

可以看到时间, 其实JIT检查到有大循环代码已经执行了指令的优化,以上是本地机器码执行的速度,只是这里没有走OSR(on stack replacement),对于OSR的讲解下次有空再说,这里不赘述。

优化后的代码是这样的:

 

  public ResponseEntity list() {
        int  count = 0;
        long time1  = System.currentTimeMillis();
        count = getCount(count);
        long time2  = System.currentTimeMillis();
        System.err.println(time2-time1);
        return ResponseEntity.ok(count);
    }

    private int getCount(int count) {
        for (int i = 1 ; i < 1000000000 ; i ++){
            count +=i ;
        }
        return count;
    }

多次执行时间如下:

 

 

我们可以看到优化前后的对比接近一倍,其实我就重构了一下方法(这里用int其实不用纠结内存不满足溢出但是不影响我们统计指令计算的效率),为什么呢?虽然是实战但是简单描述一下,每一个方法的执行都会先被PUSH如方法栈,但是如果发现有对应已被转为本地方中的方法法的地址,就不会触发方法栈替换,用编译好的本地方法代替之前栈中的方法(也就是OSR)。

到这里我们很简单的就知道JIT优化的部分思路和一些编码技巧,比如循环压缩和展开等等,本节重点是JIT关于OSR以后请持续关注。

其实笔者只是想说明在相同条件的情况下,通过不同的编程技巧可以带来比较客观的运行效率这个可能很大程度取决于程序员本生。

 

接下来我们看看LUA的执行速度:

可以看到优化后的JAVA代码其实和LUAJIT差不了多少,

lua脚本执行引擎为存C代码,整个安装包非常小巧以至于打完包不足800K,非常惊人,这里为题外话

 

总结:

  • 简单总结一下,学习到这里我们至少要明白JVM属于解释和编译型混合语言,那么只站在字节码层面,一定要说JAVA是编译型静态语言也没问题,但是要考虑到字节码之后的执行流程就不一样了,这应该是部分人的误区吧。
  • 另外,我们也应该知道JAVA大神们为了提高JAVA本身解释慢的情况引入了JIT这个组件,所以对一提到JAVA第一反映就是慢的思想,笔者也不知如何作答了。
  • 再者,笔者一直推崇语言只是工具,我们应该为解决问题去学习而不是单纯的”速度快“

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值