后端编译与优化

本书部分摘自《深入理解 Java 虚拟机第三版》

概述

前面讲过前端编译是将 Java 源代码编译成 Class 字节码,那么后端编译就对应把 Class 文件转换成与本地机器相关的二进制机器码的过程。然后 JVM 把每一条要执行的字节码交给解释器,翻译成对应的机器码,由解释器执行,Java 程序就运行起来了

即时编译器

当虚拟机发现某个方法或代码块运行特别频繁,就会把这些代码认定为热点代码(HotSpot Code),为了提高热点代码的运行效率,在运行时,虚拟机将会把这些代码编译为本地机器码,并以各种手段进行代码优化,在运行时完成这个任务的后端编译器被称为即时编译器

1. 编译对象

热点代码主要有两类:

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

对于这两种情况,编译的目标对象都是整个方法体。第一种情况,由于是依靠方法调用触发的编译,以整个方法为编译对象毫无疑问。而后一种情况,虽然编译器仍以整个方法作为编译对象,但执行入口(从方法第几条字节码执行开始执行)会稍有不同。

2. 触发条件

如何判断热点代码?是不是需要进行即时编译?这个行为称为热点探测(Hot Spot Code Detection),进行热点探测并不一定要知道方法具体被调用了多少次,目前主流的热点探测判定方式有两种:

  • 基于采样的热点探测(Sample Based HotSpot Code Detection)

    虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(某些)方法经常出现在栈顶,那这个方法就是热点方法。这种方式的好处是实现简单高效,可以很容易获取方法调用关系(将调用堆栈展开即可),缺点是很精确地确定一个方法的热度,容易受线程阻塞或别的外界因素的影响

  • 基于计数器的热点探测(Counter Based HotSpot Code Detection)

    虚拟机为每个方法(甚至代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定阈值就认为是热点方法。这种统计方式实现起来麻烦一些,不能直接获取方法的调用关系,但结果相对来说更加精确

HotSpot 采用基于计数器待热点探测方法,同时准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,回边的意思是指在循环边界往回跳转)。当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,一旦溢出,就会触发即时编译。

当一个方法被调用时,虚拟机会先检查该方法是否存在被即时编译过后的版本,如果存在,则优先使用编译后的本地代码来执行。如果不执行已被即时编译过后的版本,则将该方法的调用计数器值加一,然后判断方法调用计数器与回边计数器之和是否超过阈值,如果超过,则向即时编译器提交一个该方法的代码编译请求

如果没做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器执行字节码,直到被提交的请求被即时编译器编译完成,当编译工作完成后,这个方法的调用入口地址就会被系统自动改写成新值,下一次调用该方法就会使用已编译的版本

默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍不足以让它提交给即时编译器编译,那该方法的调用计数器就会减半,这个过程称为方法调用计数器热度的衰减(Counter Decay),这个动作是在虚拟机进行垃圾收集时顺便进行的,也可以关闭热度衰减,让虚拟机统计方法调用的绝对次数,这样时间长了,程序中绝大部分方法都会被编译成本地代码

再看一看回边计数器,它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就称为回边(Back Edge)。当解释器遇到一条回边指令,会先查找将要执行的代码片段是否有已经编译好的版本,如果有的话,将优先执行已编译的代码,否则就把回边计数器的值加一,然后判断方法调用器与回边计数器之和是否超过回边计数器的阈值。当超过阈值,将提交一个栈上替换编译请求,并且把回边计数器的值稍微降低,以便继续在解释器中执行循环,等待编译器输出编译结果

回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是方法循环执行的绝对次数。当计数器溢出时,它还会把方法计数器的值也调整为溢出状态,这样下次再进入这个方法的时候就会执行标准编译过程了

提前编译器

现在提前编译器的研究两条明显的分支:在程序运行前把程序代码编译成机器码的静态翻译工作,以及把原本即时编译器在运行时要做的编译工作提前做好并保存,下次运行到这些代码时直接把它加载进来使用

第一种传统的提前编译应用形式,它是为了解决即时编译的最大弱点:即时编译要占用程序的运行时间和时间资源。而第二种方式,本质是给即时编译器做缓存加速。

提前编译器因为没有执行时间和资源限制的压力,可以毫无忌惮地使用重负载的优化手段,这是一个极大的优势,但即时编译器也有它的长处:

  • 性能分析制导优化(Profile-Guided Optimization)

    即时编译器在运行过程中,会不断地收集性能监控信息,譬如条件判断通常走哪个分支、循环会进行几次等等,这些数据一般在静态分析时是无法得到的,或者说不能得到一个明确的解。但在动态运行时却能看出它们具有非常明显的偏好性,比如一个条件分支的某一路径执行频繁,就可以对热点代码进行优化和分配更多的资源

  • 激进预测性优化(Aggressive Speculative Optimization)

    静态优化必须保证优化前后的程序对外部可见影响(不仅仅是执行结果)是等效的,而即时编译可以不必如此保守,如果性能监控监控信息能够支持它做出一些正确的可能性很大但无法保证绝对正确的预测判断,就可以大胆地按照高概率的假设进行优化,万一真的走到罕见分支上,大不了退回到低级编译器甚至解释器上去执行,并不会出现无法挽救的后果

  • 链接时优化

    Java 语言天生就是动态链接的,一个个 Class 文件在运行期被加载到虚拟机内存中,然后在即时编译器里产生优化后的本地代码

编译器优化技术

编译器的目标虽然是做由程序代码翻译为本地机器码的工作,但其难点并不在于能否成功翻译出机器码,输出代码优化质量才是决定编译器优秀与否的关键

1. 方法内联

方法内联就是把目标方法的代码原封不动地复制到发起调用的方法之中,避免发生真实的方法调用。方法内联听上去很简单,但实现并不简单,因为有方法解析和分派机制。只有使用 invokespecial 指令调用的私有方法、实例构造器、父类方法、使用 invokestatic 指令调用的静态方法和被 final 修饰的方法,这些方法会在编译器解析。而其他 Java 方法必须在运行时进行方法接收者的多态选择,它们都有可能有多于一个版本的方法接收者

为了解决这个难题,Java 虚拟机引入了一种名为类型继承关系分析的技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。这样,如果遇到非虚方法,直接内联即可。如果查到只有一个版本,也直接内联,这种内联称为守护内联(Guarded Inlining)。不过由于 Java 程序是动态连接的,有可能会有新的类型加载进来,所以守护内联属于激进预测性优化,必须预留好退路。假如继承关系发生变化,那么就必须抛弃已编译的代码,退回到解释状态进行执行,或重新编译

如果方法存在多个版本的目标方法可供选择,虚拟机将使用内联缓存(Inline Cache)来缩减方法调用的开销。内联缓存是一个建立在目标方法正常入口之前的缓存,如果未发生方法调用,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者的版本。如果每次都一致,就直接使用,否则查找虚方法表进行方法分派

2. 逃逸分析

逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法引用,例如作为调用参数传到其他方法中,这称为方法逃逸;甚至有可能被外部线程访问,这称为线程逃逸

根据一个对象的逃逸程度,可以进行不同程度的优化:

  • 栈上分配

    对象是在栈上分配内存的,主要持有这个对象的引用,就可以访问堆中存储的对象数据。如果确定一个对象不会逃逸出线程之外,可以让这个对象在栈上分配分配内存

  • 标量替换

    若一个数据已经无法再分解成更小的数据表示,如原始数据类型,那么这些数据就称为标量。相对的,如果一个数据可以继续分解,那就称为聚合量,如对象。如果一个对象不会被方法外部访问,那这个对象就可以拆成多个标量,替换原来引用对象的成员变量的地方

  • 同步消除

    如果一个变量不会逃逸出线程,那么这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉

3. 公共子表达式消除

如果一个表达式 E 之前已经被计算过,而且从先前的计算到现在 E 中所有变量的值都没有变化,那么 E 就称为公共子表达式。之后就没有必要再花时间重新计算了,直接用之前的计算结果代替 E 即可

假设有如下代码

int d = (c * b) * 12 + a + (a + b * c);

编译器检测到 c * bb * c 是一样的表达式,而且 b 与 c 的值不变,因此这条表达式可能被视为

int d = E * 12 + a + (a + E);

4. 数组边界检查消除

我们知道 Java 中的数组不能越界访问,否则会抛出一个运行时异常,这得益于系统会自动进行上下文的范围检查。但如果每次对数组元素的读写都要检查一次,无疑是一种负担。可无论如何,数组边界安全检查是肯定要做的,不过虚拟机会在编译期根据数据分析流判断数组下标有没有越界的可能,避免过多的开销

(0)

相关推荐

  • 「技术分享」WebAssembly能否重新定义前端开发模式?

    如果提及近年来让人最为兴奋的新技术,非WebAssembly 莫属.作为一种低级的类汇编语言,WebAssembly以紧凑二进制的格式存储,为C/C++, Rust等拥有低级内存的模型语言提供了新的编 ...

  • 为什么Java程序会执行一段时间后跑的更快?

    对于Java 应用,程序员之间一个认识口口相传: 要看一个Java程序跑的快不快,需要多跑几次:另外,Java程序跑一段时间之后会快起来.速度甚至能赶上 C/C++程序的速度. 如果你问为什么跑一段时 ...

  • 基本功 | Java即时编译器原理解析及实践

    跟其他常见的编程语言不同,Java将编译过程分成了两个部分,这就对性能带来了一定的影响.而即时(Just In Time, JIT)编译器能够提高Java程序的运行速度. 本文会先解析一下即时编译器的 ...

  • 编译型语言和解释型语言的区别

    我们编写的源代码是人类语言,我们自己能够轻松理解:但是对于计算机硬件(CPU),源代码就是天书,根本无法执行,计算机只能识别某些特定的二进制指令,在程序真正运行之前必须将源代码转换成二进制指令. 所谓 ...

  • Vue2+Koa2+Typescript前后端框架教程--02后端KOA2框架自动重启编译服务(nodemon)

    上一篇讲完搭建Typescritp版的Koa框架后,F5运行服务端,页面进行正常显示服务. 今天要分享的是,如果要修改服务端代码,如果让编译服务自动重启,免去手动结束服务再重启的过程. 自动重启服务需 ...

  • 香港云服务器影响国内网站优化吗?

    香港云服务器影响国内网站优化吗?香港云服务器一直以来都以免备案.省时省力.开通即用等优势被大众喜爱,大多数企业或者个人使用香港云服务器都是为了优化,以在互联网上接单为主,但近年由于国内政策让很多优化人 ...

  • 绩效优化--员工绩效调整建议

    某企业以前没有员工绩效管理体系,因为经济效益不好全员实行绩效管理,实施一段时间后,发现员工个体层面的绩效管理效率并不高,业务部门和HR部门花费了大量的时间和精力在员工个人的绩效计划及评估上,但结果并不 ...

  • 职位薪酬优化项目记实

    公司是主要从事电力设施.电力元器件.家用电工的研产销一体企业,是国内覆盖电工产品全领域的大型公司.近年来,围绕"以跨越式发展推动公司的转型升级,实现二次发展"的目标,对公司人力资源 ...

  • 招聘分析:优化招聘的3个层次

    招聘分析,也称为招聘分析,对招聘人员和招聘经理起着越来越重要的作用.招聘分析可以在采购,选择和雇用方面帮助做出更好的,数据驱动的选择.我们将分三步介绍什么是招聘分析,以及如何实现增值分析. 1.什么是 ...

  • HR优化绩效管理的10个步骤?

    我们都认为,绩效管理的当前状态不再适用于当今的现代组织,所以公司急切地寻求下一个伟大的解决方案,它将有助于推动组织在数字世界中保持竞争力.随着我们向数字化员工队伍过渡,集成的绩效管理解决方案将成为优化 ...

  • 职能部门绩效考核优化方案

    职能部门绩效考核一直是企业绩效考核的难点,主要场景如下:1.职能部门绩效指标不好量化,主观评价比较多:2.业务线绩效得分一般都比较低,非业务线(职能部门)的绩效得分都很高:3.业务线(职能部门)的绩效 ...

  • 如何基于价值链设计和优化组织架构

    Part1 背景 由于顾客需求的变化和增加.互联网的冲击以及企业之间的高度竞争,企业要想在竞争中处于不败之地,就需要制定明确的企业战略,并通过组织结构的设计和调整优化实现目标战略.到目前为止,大多还是 ...

  • 10个CSS简写/优化技巧整理

    CSS简写就是指将多行的CSS属性简写成一行,又称为CSS代码优化或CSS缩写.CSS 简写的最大好处就是能够显著减少CSS文件的大小,优化网站整体性能,更加容易阅读. 下面介绍常见的CSS简写规则: ...