提纲
即时编译器
解释器与编译器
解释器优点:解释执行节约时间
编译器优点:编译执行提高效率
即时编译器:将热点(运行频繁的)代码编译成与本地平台相关的机器码,并进行各层次的优化。完成这个任务的编译器成为即时编译器。
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