深入理解jvm--晚期优化

提纲

即时编译器

解释器与编译器

解释器优点:解释执行节约时间

编译器优点:编译执行提高效率

即时编译器:将热点(运行频繁的)代码编译成与本地平台相关的机器码,并进行各层次的优化。完成这个任务的编译器成为即时编译器。

hotspot虚拟机内置两个即时编译器,client compiler和server compiler,简称为C1编译器和C2编译器。hotspot会根据自身版本与宿主机的性能自行选择运行模式。也可使用-client和-server去强制虚拟机运行在client模式与server模式。

解释器与编译器搭配使用的方式在虚拟机中称为混合模式。可使用-Xint强制运行解释模式,也可使用-Xcomp强制使用编译模式。

为了在程序启动响应速度与运行效率之间达到最佳平衡,hotspot会逐渐采用分层编译的策略。

分层编译:第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译。

          第1层,称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。

          第2层,称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

编译对象与触发条件

热点代码是被多次调用的方法时,编译器会以整个方法作为便宜对象,这种编译也是虚拟机中标准的JIT编译方式。

热点代码是被多次执行的循环体时,尽管编译动作是由循环体触发的,但编译器依然会以整个方法作为编译对象,这种编译方式因为发生在方法执行过程中,因此形象地称之为栈上替换,即OSR编译。

热点探测:判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测。

热点探测判定方式:

    基于采样的热点探测:周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法。优点:实现简单、高效,缺点:很难精确地确认一个方法的热度。

    基于计数器的热点探测:为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。缺点:实现麻烦,优点:统计结果更加精确。

基于计数器的热点探测:

    方法调用计数器:统计方法被调用的次数。 如果不做任何设置,方法调用计数器统计的不是方法被调用的绝对次数,而是一个相对的执行频率。当超过一定的时间额度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间称为此方法的半衰周期。

参数:关闭热度衰减:-XX:UseCounterDecay

               设置半衰周期:-XX:CounterHalfLifeTime

              设置阈值:-XX:CompileThreshold

    回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边。

参数:设置阈值:-XX:BackEdgeThreshold

              设置OSR比率:-XX:OnStackReplacePercentage

         在client模式下,回边计数器阈值=方法调用计数器阈值xOSR比率/100

         在server模式下,回边计数器阈值=方法调用计数器阈值x(OSR比率-解释器监控比率)/1000

编译过程

client compiler:第一阶段,平台独立的前端将字节码构造成一种高级中间码(HIR)来表示;第二阶段,一个平台相关的后端从HIR中产生低级中间码(LIR)表示;第三阶段是在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并做窥孔优化,产生机器码。

server compiler:会执行所有经典的优化动作。还会根据解释器或client compiler提供的性能监控信息,进行不稳定的激进优化。

编译优化技术

公共子表达式消除

如果一个表达式E已经计算过了,并且从现前计算到现在E中所有变量值都没有发生变化,那么E的这次出现就成为公共子表达式。可直接用表达式结果替代E。如果仅限于程序的基本块内,便被称为局部公共子表达式消除。如果优化范围涵盖多个基本块,就称为全局子表达式消除。

数组边界检查消除

如果编译器通过数据流分析就可以判断循环变量的取值范围永远在[0,length)内,那在多次循环中就可以把上下界检查消除。

方法内联

把目标方法的代码赋值到调用的方法中。

类型继承关系分析(CHA):用于确定在目前已加载的类中,某个接口是否有多种实现,某个类是否存在子类、子类是否为抽象类等信息。

如果方法时非虚方法,那么直接进行内联就可以。如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择。如果只有一个版本,那也可以进行内联,这种内联属于激进优化,需要预留逃生门,称为守护内联。若有多个版本,则放到内联缓存中,若两次调用版本不同,则进行方法分派。

逃逸分析

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

证明一个对象不会逃逸到方法或线程之外,则可为这个变量进行以下优化:

   栈上分配:如果确定一个对象不会逃逸出这个方法之外,那让这个对象在栈上分配内存将会是一个不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。

   同步消除:如果确定一个对象不会逃逸出这个方法之外,对这个变量的同步措施也可消除。

   标量替换:如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。

参数:开启逃逸分析:-XX:+DoEscapeAnalysis

   查看分析结果:-XX:+PrintEscapeAnalysis

   开启标量替换:-XX:+EliminateAllocation

   开启同步消除:-XX:+EliminateLocks

   查看标量替换情况:-XX:+PrintEliminateAllocation


猜你喜欢

转载自blog.csdn.net/yinweicheng/article/details/80918264
今日推荐