zoukankan      html  css  js  c++  java
  • java中的即时编译(JIT)简介

    Java发展这么多年一直长青,很大一部分得益于开发人员长期对其坚持不懈的优化:写得更少,跑得更快!JIT就是其中一项十分重要的优化。

    JIT全程Java Intime Compiler,即Java即时编译器。咦为啥Java的编译器是一项优化呢?Java本来不就是编译型语言吗?听我细细道来。

    从我们最早接触Java编程开始,学习到的就是手写java文件,然后javac编译、java运行主方法。

    如果这里都看不懂可能不适合阅读本文

    javac会把.java文件编译成.class文件,所以我们说Java是编译型语言。当然Java是强类型的语言,通常我们说强类型的是编译型的,弱类型的脚本语言(也叫动态语言,相对应的强类型语言叫静态语言)。.class文件格式就是“字节码”。编译的过程见《Java文件的编译》

    为了实现“一次编写,随处运行”的目标,字节码会被jvm运行。而这里的运行就是解释执行,jvm是一行一行阅读字节码文件中的jvm指令,并把它翻译成机器的cpu指令。这个过程就比较慢了(相对中低级语言而言)。

    Java为了提高开发和运行效率,已经对语言和jvm在多方面做了优先,包括垃圾回收器、各种锁机制,甚至最简单的分支预测都大力优化。解释执行的效率自然也被纳入优化范围。在1996年10月25号,当时的Java东家Sun发布了第一款JIT编译器。那时还是java 2刚出来(Java1 和Java2差异较大,我们现在使用的jdk都是Java2上的迭代),离现在已经20多年了。目前JIT已经是默认开启的,因为它带来的效果明显。除非通过参数指定不使用。

    JIT的动机基于“二八定律”,20%的热点代码占据了程序80%的执行时间

    即使开启了JIT,也少不了代码编译和字节码解释的过程。JIT处理的是热点代码(hotspot code,或叫热门代码)。热点代码就是频繁执行的代码块,比如循环里面的代码。JIT有一套逻辑判断是否热点代码。

    既然JIT处理后的是机器能够快速执行的代码,为啥还要解释执行呢,干嘛不把全部代码编译成机器代码呢?这是由于编译本地代码比较费时间,而且编译后还要进行进一步的优化导致耗时更久;而解释器是能够立即解释字节码文件的,毕竟我们的应用放到服务器上的时候就已经是字节码文件了,解释器可以拿来直接用。而且解释器执行的时候占用的内存更小,在内存受限的场景难以使用编译器(比如手机上)。编译器会概率性地选择多数时候都能提升运行效率的手段进行优化,如果“优化”后发现还不如不优化(甚至执行有问题)就得“逆优化”,回退到解释执行状态。

    我们可以通过最简单的查看Java版本的命令查看Java是否使用了编译器:

     ~ > java -version
    
    java version "1.8.0_191"
    Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
    Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
    

    最后输出的“mixed mode”代表是混合模式,也就是先解释执行,并逐步将热点代码代替为机器代码。不使用编译器的模式叫“interpreted mode”;优先使用编译器的模式叫“compiled mode”,compiled mode会优先采用编译方式执行程序,如果编译执行有问题就回退到解释执行。

    谷歌的V8是没有解释器的(没错,就是那个执行JS的)。V8的原理可以参阅这个 Quora问答

    理论上讲,经过JIT的Java程序运行效率要高于C++。因为C++是静态编译,而JIT在运行中可以参考运行时数据。

    HotSpot虚拟机中有两个编译器,一个是给客户端用的叫client Compiler,另一个是服务器用的叫Server Compiler。一般的,把Client Compiler也叫C1编译器,Server Compiler叫C2编译器或Opto编译器。虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用 “-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

    热点探测

    热点代码有两类:

    • 被多次执行的方法
    • 多次执行的循环体

    怎么统计“多次”呢?虚拟机为每个代码块和方法设置了计数器,执行一次就加1。超过限定次数就认为是热点代码,开始JIT处理。给JIT去处理只是一个请求,并不会立即同步等待结果。因为JIT编译比较耗时,在编译完成前会继续解释执行。编译器处理都是以方法为单位,所以第一类热点代码是标准的JIT编译方式;对于第二种热点代码,JIT编译器会处理包含该循环的方法。流程很简单,细节很复杂。下图来自极客学院:javac 编译与 JIT 编译

    jit
    考虑这个问题:方法在执行时会被放到栈上,对于计算密集型的方法,大量计算任务都在一个方法内循环。这满足第二类热点代码,会被编译。但是方法并没有退出重新执行,编译后的代码怎么能够执行呢?

    这个对于早期的JIT的确是个问题,不过现在JVM用到了”栈上替换“的技术:在执行过程中如果编译版本可用了,虚拟机会暂停,把编译版本的方法替换到栈上。反之亦然,上面说过逆优化。

    那到底是超过多少次?
    HotSpot虚拟机有两种计数器(方法会同时记录这两个计数),它们的阈值并不同。

    • 调用次数计数器,可以通过-XX:CompileThreadhold参数指定阈值,不指定默认C1是1500次,C2是1万次。
    • 字节码中向之前跳转的指令叫“回边”,回边次数是回边计数器。明显这个针对的是第二类热点代码。它的阈值是算出来的,公式如下
    OSR 阈值 = CompileThreshold * 
    ((OnStackReplacePercentage - InterpreterProfilePercentage)/100)
    

    第一个参数CompileThreshold就是调用计数器,后面两个也都可以通过-XX指定。默认InterpreterProfilePercentage是33,而OnStackReplacePercentage的默认值在客户端和服务器模式不一样,分别是933和140,所以阈值分别是13500和10700。

    分层编译(Tiered Compilation)

    Tiered Compilation是Java7中出现的,目的是整合C1的快速编译和C2的快速执行。因为C2使用了“激进”的优化手段,编译较慢。Java7以前,一般要求快速启动的GUI程序会选择C1,偏好性能的服务器程序使用C2。

    Tiered Compilation将编译分为0到4五级,怎么区分呢?看图吧,我并不太懂(出处见水印,侵删):

    Tiered Compilation
    好吧其实图中并没他们的区别,只是有无profiling而已。

    java 10中引入了编译更慢的Graal代替C2成为了第五级编译器。C2是用C++编写的,Graal是Java编写的。只是两种语言而已,为什么要用Java重写一个编译器呢?

    因为C2中的全部优化能力已经全部移植到了Graal上,而Graal上面有一些算法(比如inlining算法及partial escape analysis)并不能用C++实现。

    Inlining被称为优化之母,因为它能引发更深的优化,能将对getter、setter的访问优化成单一内存访问。

    常见的逃逸分析针对的就是锁去除。如果对象被单一线程访问,则可去除锁;如果对象是堆分配且仅被单一方法访问,则可转化成栈分配,并伴随将对字段的访问替换成对操作数的访问,从而进一步将栈分配转换成虚拟分配。另外一大逃逸分析场景是for-loop。

    Java10默认激进优化器依然是C2,要使用Graal需要使用参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler来开启。

    参考文献:

    动态编译与性能测量

    Java 即时编译器JIT机制以及编译优化

  • 相关阅读:
    Palindrome Partitioning
    triangle
    Populating Next Right Pointers in Each Node(I and II)
    分苹果(网易)
    Flatten Binary Tree to Linked List
    Construct Binary Tree from Inorder and Postorder Traversal(根据中序遍历和后序遍历构建二叉树)
    iOS系统navigationBar背景色,文字颜色处理
    登录,注销
    ios 文字上下滚动效果Demo
    经常崩溃就是数组字典引起的
  • 原文地址:https://www.cnblogs.com/somefuture/p/14272221.html
Copyright © 2011-2022 走看看