七 晚期(运行期)优化
1 即时编译器(JIT编译器)
---当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”,包括被多次调用的方法和被多次执行的循环体。
---为了提高热点代码的执行效率,在运行时,虚拟机就会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)。
---即时编译器不是虚拟机必需的部分,虚拟机规范对其没有做任何规定,与具体的虚拟机实现有关。
2 解释器与编译器
---许多主流的商用虚拟机如HotSpot,都同时包含解释器和编译器。
---解释器和编译器两者各有优势:
· 当程序需要迅速启动和执行的时候。解释器可以首先发挥作用,省去编译的时间,立即执行;
· 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率;
· 当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之,可以使用编译执行提升效率;
· 解释器还可以作为编译器激进优化时的一个“逃生门”。
---HotSpot虚拟机中内置了两个即时编译器:Client Compiler(C1编译器)、Server Compiler(C2编译器)。默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器取决于虚拟机运行的模式。
---分层编译策略:
· 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译;
· 第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑;
· 第2层,也称为C2编译。将字节码编译为本地代码,会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
3 编译对象和触发条件
---JIT编译方式:方法被多次调用触发的编译,编译器以整个方法作为编译对象。
---栈上替换(OSR编译):循环体被多次执行触发的编译,编译器同样以整个方法作为编译对象。
---热点探测:判断一段代码是不是热点代码,是不是需要触发即时编译。目前主要的判定方式有两种:
· 基于采样的热点探测:虚拟机会周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。
---优点:实现简单、高效,可以很容易地获取方法调用关系。
---缺点:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
· 基于计数器的热点探测:虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。
---优点:统计结果更加精确和严谨。
---缺点:实现比较复杂,不能直接获取到方法调用关系。
---在HotSpot虚拟机中使用的是基于计数器的热点探测,它为每个方法准备了两类计数器:方法调用计数器和回边计数器。
---方法调用计数器:统计方法被调用的次数,在Client VM下,其触发的即时编译过程如下:
---方法调用计数器热度的衰减:当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少到一半。一般是在虚拟机进行垃圾收集时顺便进行的。也可以设置为不进行热度衰减。
---回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。
---回边计数器阈值计算公式:
· 虚拟机运行在Client模式下,计算公式为:方法调用计数器阈值*OSR比率/100;
· 虚拟机运行在Server模式下,计算公式为:方法调用计数器阈值*(OSR比率-解释器监控比率)/100。
---在Client VM下,回边计数器触发的即时编译过程如下:
---回边计数器没有热度衰减的过程。当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程(JIT编译)。
4 编译过程
---Server Compiler和Client Compiler两个编译器的编译过程是不一样的。
---Client Compiler主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段,过程如下:
· 一个平*立的前端将字·节码构造成一种高级中间代码表示(HIR),在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等;
· 一个平台相关的后端从HIR中产生低级中间代码表示(LIR),在此之前编译器会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等;
· 在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。
5 编译优化技术
---编译方式执行本地代码比解释方式更快的原因:
· 虚拟机解释执行字节码时需要额外的消耗时间;
· 虚拟机对代码的优化措施几乎都集中在即时编译器中。
(1)方法内联
---主要目的是:
· 去除方法调用的成本,如建立栈帧等;
· 为其他优化建立良好的基础,方法内联膨胀之后可以便于在更大范围上采取后续的优化手段,从而获取更好的优化效果。
---Java虚拟机的内联过程:
· 编译器在进行方法内联时,如果是非虚方法,那么可以直接进行内联。
· 如果是虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,若查询结果只有一个版本,那也可以进行内联,但是这种内联属于激进优化,需要预留一个“逃生门”,称为守护内联。如果在程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去。但如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译。
· 如果CHA查询出来的结果是有多个版本的目标方法可供选择,则编译器会使用内联缓存来完成方法内联。
---注1:类型继承关系分析(CHA)技术:用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。
---注2:内联缓存工作原理:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下来方法接收者的版本信息,并且每次进行方法调用时都比较接收者版本,如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去。如果发生了方法接收者版本不一致的情况,就说明程序真正使用了虚方法的多态特性,这时会取消内联,查找虚方法表进行方法分派。
(2)公共子表达式消除
---含义是:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替E就可以了。
---语言无关的经典优化技术之一。
(3)数组边界检查消除
---语言相关的经典优化技术之一。
---把运行期检查提到编译期完成。
(4)逃逸分析
---逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸;甚至还有可能被外部线程访问到,称为线程逃逸。
---如果一个对象不会逃逸到方法或者线程之外,则可能为该变量进行如下优化:
· 栈上分配:如果一个对象不会逃逸出方法之外,那么可以让这个对象在栈上分配内存。
· 同步消除:如果一个对象不会逃逸出线程之外,那么可以将对这个对象的同步措施消除掉。
· 标量替换:如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。
---注1:标量:一个数据已经无法再分解成更小的数据来表示,如Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)。
---注2:聚合量:一个数据可以继续分解,如Java中的对象。