第11章 晚期(运行期)优化

1、概述:

虚拟机会根据代码执行情况,如果代码执行特别频繁,就将这段代码编译成本地平台相关的机器码,完成这个任务的编译器就是即时编译器(Just In Time Compiler)简称JIT编译器,涉及的虚拟机是指HotSpot虚拟机的即时编译器。

2、HotSpot虚拟机内的即时编译器

2.1 解释器与编译器

解释器:程序可以迅速启动和执行,消耗内存小 (类似人工 成本地,到后期效率低)

编译器:随着代码频繁执行会执行将代码编译成本地机器码  (类似机器,成本高,到后期效率高)

它们可以进行互补

第11章 晚期(运行期)优化

当然也可以强制指定

三种模式:

  • 1、混合模式(Mixed Mode): 默认模式
  • 2、解释模式(Interpreted Mode): 使用参数-Xint强制
  • 3、编译模式(Compiled Mode): 使用参数-Xcomp

第11章 晚期(运行期)优化

为了解决编译过程占用程序运行时间的过长,HotSpot虚拟机将会逐渐启用分层编译策略。

第0层:程序解释执行

第1层:C1编译,(将字节码编译为本地代码)简单优化 (编译速度)

第2层:C2编译,深度优化,激进优化 (编译质量)

类似:不断迭代,由易入难

2.2 编译对象与触发条件

如何评判为热点代码:

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

第一种:编译整个方法。

第二种:编译整个方法,但是编译发生在执行过程中,所以称为栈上替换(On Stack Replacement OSR)

问题是多次到底是多少次,用什么来方法来评判?

热点探测(Hot Spot Detection):有两种:

  • 1、基于采样的热点探测:周期性检查各个线程的栈顶,来计算那个方法出现次数多。简单,缺点:受到线程阻塞影响/
  • 2、基于计数器的热点探测:为每个方法增加计数器(方法调用计数器和回边计数器【表示循环次数】)

HotSpot采用的是第二种

当计数器超过阈值,就会触发JIT编译。

方法调用计数器:

Client模式默认值阈值为1500次,在Server模式下是10000次。

也可以通过-XX:CompileThreshold 人工设置 ,注意这个计数跟时间有关系的。

(记得玩一款消消乐游戏,有时间限制,但是如果你消除块会增加时间,没有消除它时间会默默减少,直到结束,这个有点类似)

如果超过一定的时间限定,记录的次数不足触发编译。这个方法计数器的次数减少一半(这个称为热度衰减)这时期为半衰周期,当然你可以设置没有衰减,采用绝对次数,通过设置-XX:-UseCounterDecay关闭热度衰减,-XX:CounterHalfLifeTime 参数设置半衰期周期单位是秒。

第11章 晚期(运行期)优化

回边计数器:

-XX:BackEdgeThreshold设置阈值(虚拟机没有用),-XX:OnStackReplacePercentage来间接调整回边计数器的阈值。

Client模式下: 阈值=方法调用计数器阈值*OnStackReplacePercentage/100 (OnStackReplacePercentage默认值为933),都取默认值那么这个阈值为13995.

Server模式下:阈值=(方法调用计数器阈值*OnStackReplacePercentage-InterpreterProfilePercentage)/100,其中OnStackReplacePercentage默认值为140,InterpreterProfilePercentage默认值为33, 结果阈值为10700

注意:它统计的是绝对次数。

2.3、编译过程

默认情况下,如果编译器没有完成编译工作,它还是会采用解释方式继续执行,而编译动作会在后台继续执行,当然你也可以设置参数-XX:-BackgroundCompilation 来禁止后台编译,当达到JIT要求时候,会等待编译完再执行(让我想起小时候,不给我买这个玩具就是不走。O(∩_∩)O哈哈~)

Client Compiler 和Server Compiler 编译过程不一样。

Client Compiler: 怎么快,怎么来的,顺便优化一下。(三段式编译器)

第一阶段:字节码构造高级中间代码表示(HIR),HIR使用静态单分配(SSA)来代表代码值。

第二阶段:后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation LIR) 如果空值检查消除、范围检查消除,以便让HIR达到更高效的代码形式。

第三阶段:后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码。

Server Compiler: 优化到极致(不惜采取偏激手段)

无用代码消除、循环展开、循环表达式外提、、、、、

3、编译优化技术

  • 语言无关的经典优化技术之一:公共子表达式消除
  • 语言相关的经典优化技术之一:数组范围检查消除
  • 最重要的优化技术之一:方法内联
  • 最前沿的优化技术之一:逃逸分析

3.1 公共子表达式消除

也就是算出一遍的结果不需要再次重复算一遍 : 分为局部公共子表达式消除和全局公共子表达式消除

例如:

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

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

int d = E*13 + a*2;

这样节省时间

3.2 数组边界检查消除

编译器只要通过数据流分析可以判定循环变量的取值范围永远在区间[0,foo.length)之内,那么整个循环中就可以把数组的上下界检查消除掉,自动装箱消除、安全点消除、消除反射

3.3 方法内联

方法相互调用,优化难度大

3.4 逃逸分析

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为称为方法逃逸。赋值给类变量或可以在其他线程中访问的实例变量,这个行为称为线程逃逸。

如果该方法无法逃逸,可以进行优化。

栈上分配(Stack Allocations) :在栈上分配内存, 随着方法结束而自动销毁变量。(减少垃圾回收)

同步消除(Synchronization Elimination):不会出现并发的情况(消除同步)

标量替换:数据最小单位例如原始数据类型称为变量。 可以继续分解为聚合量,如果这个对象不会逃逸,不会创建对象,而是创建若干个成员变量来替换。

用户可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析, 开启后通过参数: -XX:+PrintEscapeAnalysis来查看分析结果,用户可以通过使用参数-XX:EliminateAllocations来开启标量替换,使用参数+XX:+EliminateLocks来开启同步消除,使用参数 -XX:+PrintEliminateAllocations来查看标量的替换情况。