0
点赞
收藏
分享

微信扫一扫

Java技术专题-彻底你明白什么是JIT编译器(Just In Time编译器)

前提概要

  • 我们都知道开发语言整体分为两类,一类是编译型语言,一类是解释型语言。那么你知道二者有何区别吗?编译器和解释器又有什么区别?

  • 这是为了兼顾启动效率和运行效率两个方面。Java程序最初是通过解释器进行解释运行的,当虚拟机返现某个方法或代码块的运行特别频繁时,就会把这段代码标记为热点代码,为了提供热点代码的运行效率,在运行时,虚拟机就会把这些代码编译成与本地平台相关的机器码。并进行各种层次的优化。

编译器和解释器

  • Java编译器(javac)的作用是将java源程序编译成中间代码字节码文件,是最基本的开发工具。
  • Java解释器(java)(英语:Interpreter),又译为直译器,是一种电脑程序,能够把高级编程语言一行一行直接转译运行。解释器不会一次把整个程序转译出来,只像一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。 它每转译一行程序叙述就立刻运行,然后再转译下一行,再运行,如此不停地进行下去。

  1. 当程序需要首次启动和执行的时候,解释器可以首先发挥作用,一行一行直接转译运行,但效率低下。
  2. 当多次调用方法或循环体时JIT编译器可以发挥作用,把越来越多的代码编译成本地机器码,之后可以获得更高的效率(占内存),此时就有了智能化的编译器(JIT编译器)

解释器与编译器的交互:

什么是JIT编译器

  • 即时(Just-In-Time)编译器是Java运行时环境的一个组件,它可提高运行时Java应用程序的性能。JVM中没有什么比编译器更能影响性能,而选择编译器是运行Java应用程序时做出的首要决定之一。

  • 当编译器做的激进优化不成立,如载入了新类后类型继承结构出现变化。出现了罕见陷阱时能够进行逆优化退回到解释状态继续运行。

解释器与编译器搭配使用的方式:

  • -client:强制虚拟机运行Client模式
  • -server:强制虚拟机运行Server模式
  • 默认(java -version混合模式)
  • 解释模式(java -Xint -version)强制虚拟机运行于解释模式,仅使用解释器方式执行。
  • 编译模式(java -Xcomp -version)优先采用编译方式执行程序,但解释器要在编译无法进行的情况下介入执行过程。
java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
java -Xint -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, interpreted mode)
java -Xcomp -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, compiled mode)

  • 解释字节码的是Java虚拟机(JVM)的标准实现,这会使程序的执行速度变慢。为了提高性能,JIT编译器在运行时与JVM交互,并将适当的字节码序列编译为本地机器代码。

    • 使用JIT编译器时,硬件可以执行本机代码,而不是让JVM重复解释相同的字节码序列,并导致翻译过程相对冗长。这样可以提高执行速度,除非方法执行频率较低。

    • JIT编译器编译字节码所花费的时间被添加到总体执行时间中,并且如果不频繁调用JIT编译的方法,则可能导致执行时间比用于执行字节码的解释器更长。

  • 当将字节码编译为本地代码时,JIT编译器会执行某些优化。

  • 由于JIT编译器将一系列字节码转换为本机指令,因此它可以执行一些简单的优化。

    • JIT编译器执行的一些常见优化操作包括数据分析,从堆栈操作到寄存器操作的转换,通过寄存器分配减少内存访问,消除常见子表达式等。

    • JIT编译器进行的优化程度越高,在执行阶段花费的时间越多。

  • JIT编译器默认情况下处于启用状态,并在调用Java方法时被激活。

  • JIT编译器将该方法的字节码编译为本地机器代码,“即时”编译以运行。

  • 编译方法后,JVM会直接调用该方法的已编译代码,而不是对其进行解释。

JIT编译确实需要处理器时间和内存使用率。JVM首次启动时,将调用数千种方法。即使程序最终达到了非常好的峰值性能,编译所有这些方法也会严重影响启动时间。

不同应用程序的不同编译器

客户端编译器

服务器端编译器

分层编译

为什么要进行分层编译
  • 分层编译结合了客户端和服务器端编译。分层编译利用了JVM中客户端和服务器编译器的优势

  • 客户端编译器在应用程序启动期间最活跃,并处理由较低的性能计数器阈值触发的优化

  • 客户端编译器还会插入性能计数器,并为更高级的优化准备指令集,服务器端编译器将在稍后阶段解决这些问题。

分层策略例如以下所看到的:
  • 第0层:程序解释运行。解释器不开启性能监控功能,可触发第1层编译。
  • 第1层:即C1编译。将字节码编译为本地代码。进行简单和可靠的优化,如有必要将增加性能监控的逻辑。
  • 第2层:即C2编译,将字节码编译为本地代码,同一时候启用一些编译耗时较长的优化,甚至会依据性能监控信息进行一些不可靠的激进优化。

代码优化

  • 当选择一种方法进行编译时,JVM会将其字节码提供给即时编译器(JIT)。JIT必须先了解字节码的语义和语法,然后才能正确编译该方法。

    • 为了帮助JIT编译器分析该方法,首先将其字节码重新格式化为称为trees,它比字节码更类似于机器代码。

    • 然后对方法的树进行分析和优化

    • 最后,将树转换为本地代码。

  • JIT编译器可以使用多个编译线程来执行JIT编译任务,使用多个线程可以潜在地帮助Java应用程序更快地启动。

编译包括以下阶段:

内联
局部优化
控制流优化
全局优化
本机代码生成

编译对象

  • 被多次调用的方法
  • 被多次运行的循环体

触发条件

热点探测有两种手段:

基于采样的热点探测(Sample Based Hot Spot Dectection)

基于计数器的热点探测(Counter Based Hot Spot Dectection)

方法调用计数器

  • 方法调用次数统计的并非方法被调用的绝对次数,而是相对的运行频率,即一段时间内方法被调用的次数,当超过一定时间限度,假设方法的调用次数仍然不足以让它提交给即时编译器编译,那这种方法的调用计数器会被降低一半,这个过程被称为方法调用计数器的热度衰减(Counter Decay)。

  • 而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。相同也能够使用參数-XX:-UseCounterDecay来关闭热度衰减。

方法调用计数器触发即时编译的整个流程例如以下图所看到的:

回边计数器

什么是回边?
  • 回边计数器是用来统计一个方法中循环体代码运行的次数,回边计数器的阈值能够通过參数-XX:OnStackReplacePercentage来调整。
虚虚拟机运行在Client模式下,回边计数器阂值计算公式为:
方法调用计数器闭值(CompileThreshold) xOSR比率(OnStackReplacePercentage) / 100

当中OnSlackReplacePercentage默认值为933,假设都取默认值,那Client模式虚拟机的回边计数器的阂值为13995。

虚拟机运行在Server模式下,回边计数器阂值的itm公式为:
方法调用计数器阂值(CompileThreshold) x (OSR比率(OnStackReplacePercentage) - 解释器监控比率(InterpreterProffePercentage) / 100
  • 当中OnSlackReplacePercentage默认值为140。 InterpreterProffePercentage默认值为33.

  • 假设都取默认值。BF Server模式虚拟机回边计数器的阈值为10700。

回边计数器触发即时编译的流程例如以下图所看到的:

回边计数器与方法调用计数器不同的是,回边计数器没有热度衰减,因此这个计数器统计的就是循环运行的绝对次数。

编译流程

在默认设置下,不管是方法调用产生的即时编译请求,还是OSR编译请求,虚拟机在代码编译器还未完毕之前,都仍然依照解释方式继续进行,而编译动作则在后台的编译线程中继续进行。也能够使用-XX:-BackgroundCompilation来禁止后台编译,则此时一旦遇到JIT编译,运行线程向虚拟机提交请求后会一直等待,直到编译完毕后再開始运行编译器输出的本地代码。

那么在后台编译过程中,编译器做了什么事呢?

Client Compiler编译流程

  • 第一阶段:一个平台独立的前端将字节码构造成一种高级中间码表示(High Level Infermediate Representaion),HIR使用静态单分配的形式来表示代码值,这能够使得一些的构造过程之中和之后进行的优化动作更easy实现,在此之前编译器会在字节码上完毕一部分基础优化,如方法内联、常量传播等。

  • 第二阶段:一个平台相关的后端从HIR中产生低级中间代码表示(Low Level Intermediate Representation),而在此之前会在HIR上完毕还有一些优化。如空值检查消除、范围检查消除等。以便让HIR达到更高效的代码表示形式。

  • 第三阶段:在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔优化(Peephole)优化,然后产生机器码。

举报

相关推荐

0 条评论