精华推荐 |JVM技术专题深入学习JIT编译器实现机制「核心剖析篇」

Posted 洛神灬殇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精华推荐 |JVM技术专题深入学习JIT编译器实现机制「核心剖析篇」相关的知识,希望对你有一定的参考价值。

前提概要

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

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

编译器和解释器

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

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

解释器与编译器的交互:

HotSpot虚拟机中内置了两个即时编译器,分别称为Client Complier和Server Complier,
它会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用"-client"或
"-server"参数去强制指定虚拟机运行在Client模式或Server模式

什么是JIT编译器

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

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

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

HotSpot JVM内置了两个编译器,各自是Client Complier和Server Complier,虚拟机默认是Client模式。我们也能够通过。

  • -client:强制虚拟机运行Client模式
  • -server:强制虚拟机运行Server模式
  • 默认(java -version混合模式)

而不管是Client模式还是Server模式,虚拟机都会运行在解释器和编译器配合使用的混合模式下。能够通过。

  • 解释模式(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功能“一次编译,到处运行”的关键是 bytecode。字节码转换为应用程序的机器指令的方式对应用程序的速度有很大的影响。这些字节码可以被解释,编译为本地代码,或者直接在指令集架构中符合字节码规范的处理器上执行。

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

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

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

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

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

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

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

因此,JIT编译器无法承担所有静态编译器所做的优化,这不仅是因为增加了执行时间的开销,而且还因为它只对程序进行了限制。

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

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

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

从理论上讲,如果编译不需要处理器时间和内存使用量,则编译每种方法都可以使Java程序的速度接近本机应用程序的速度。

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

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

JIT编译器有两种形式,并且选择使用哪个编译器通常是运行应用程序时唯一需要进行的编译器调整。实际上,即使在安装Java之前,也要考虑知道要选择哪个编译器,因为不同的Java二进制文件包含不同的编译器。

客户端编译器

著名的优化编译器是C1,它是通过-clientJVM启动选项启用的编译器。顾名思义,C1是客户端编译器。它是为客户端应用程序设计的,这些客户端应用程序具有较少的可用资源,并且在许多情况下对应用程序启动时间敏感。C1使用性能计数器进行代码性能分析,以实现简单,相对无干扰的优化

服务器端编译器

对于长时间运行的应用程序(例如服务器端企业Java应用程序),客户端编译器可能不够。可以使用类似C2的服务器端编译器。通常通过将JVM启动选项添加-server到启动命令行来启用C2 。由于大多数服务器端程序预计将运行很长时间,因此启用C2意味着您将能够比使用运行时间短的轻量级客户端应用程序收集更多的性能分析数据。因此,您将能够应用更高级的优化技术和算法。

分层编译

为什么要进行分层编译

这是由于编译器编译本机代码须要占用程序运行时间,要编译出优化程度更高的代码锁花费的时间可能更长,并且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息。这对解释运行的速度也有影响。为了在程序启动响应速度和运行效率之间寻找平衡点。因此採用分层编译的策略。

  • 分层编译结合了客户端和服务器端编译。分层编译利用了JVM中客户端和服务器编译器的优势

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

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

分层编译是一种非常节省资源的性能分析方法,因为编译器能够在影响较小的编译器活动期间收集数据,以后可以将其用于更高级的优化。与仅使用解释的代码配置文件计数器所获得的信息相比,这种方法还可以产生更多的信息。

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

代码优化

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

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

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

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

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

编译线程的默认数量由JVM标识,并且取决于系统配置。如果生成的线程数不是最佳的,则可以使用该XcompilationThreads选项覆盖JVM决策。

编译包括以下阶段:

内联

内联是将较小方法的树合并或“内联”到其调用者的树中的过程。这样可以加速频繁执行的方法调用。

局部优化

局部优化可以一次分析和改进一小部分代码。许多本地优化实现了经典静态编译器中使用的久经考验的技术。

控制流优化

控制流优化分析方法(或方法的特定部分)内部的控制流,并重新排列代码路径以提高其效率。

全局优化

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

本机代码生成

本机代码生成过程因平台架构而异。通常,在编译的此阶段,将方法的树转换为机器代码指令;根据架构特征执行一些小的优化。

编译对象

编译对象即为会被编译优化的热点代码。有下面两类

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

触发条件

这就牵扯到触发条件这个概念,推断一段代码是否是热点代码。是否须要触发即时编译,这样的行为成为热点探测(Spot Dectection)。

热点探测有两种手段:

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

虚拟机会周期性的检查各个线程的栈顶,假设发现某些方法常常性的出如今栈顶,那么这种方法就是热点方法。

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

虚拟机会为每一个方法或代码块建立计数器,统计方法的运行次数。假设运行次数超过一定的阈值就觉得他是热点方法。

HotSpot JVM使用另外一种方法基于计数器的热点探測方法。它为每一个方法准备了两类计数器:

方法调用计数器

这个阈值在Client模式下是1500次。在Server模式下是10000此,这个阈值能够通过參数-XX:CompileThreshold来人为设定。

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

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

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

回边计数器

什么是回边?

在字节码遇到控制流向后跳转的指令称为回边(Back Edge)。

  • 回边计数器是用来统计一个方法中循环体代码运行的次数,回边计数器的阈值能够通过參数-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)优化,然后产生机器码。

以上是关于精华推荐 |JVM技术专题深入学习JIT编译器实现机制「核心剖析篇」的主要内容,如果未能解决你的问题,请参考以下文章

精华推荐 |深入浅出Sentinel原理及实战「基础实战专题」零基础实现服务流量控制实战开发指南

深入浅出 JIT 编译器

精华推荐 |深入浅出Sentinel原理及实战「基础实战专题」零基础探索分析Sentinel控制台开发指南

JVM技术专题 深入分析字节码指令重排序技术「原理篇」

JVM技术专题「源码专题」深入剖析JVM的Mutex锁的运行原理及源码实现(底层原理-防面试)

精华推荐 |深入浅出Sentinel原理及实战「原理探索专题」完整剖析Alibaba微服务架构体系之轻量级高可用流量控制组件Sentinel