晚期(运行期)优化

JIT:Java程序最初是由解释器解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些“热点代码”编译成机器码,提高运行效率。

1. 为什么要使用解释器和编译器并存?

  • 当需要程序快速启动和执行的时候,可以使用解释器,省去编译时间,立即执行。随着时间推移,编译器逐渐发挥作用,编译为机器码,可以提高效率。
  • 内存限制较大时,解释器执行可以节约内存,反之编译器执行可以提高效率。
  • 解释器可以作为编译器激进优化的逃生门。

2. HotSpot的两个不同的即时编译器

  • Client Compiler:C1编译器
  • Server Complier:C2编译器
  1. interpreted mode:纯解释模式
  2. compiled mode:纯编译模式,无法编译是解释器还是会介入的
  3. mixed mode:解释器和编译器搭配

要想编译出优化程度高的代码,需要时间成本,所以虚拟机为了权衡,采用了分层编译。采用分层编译后,C1和C2同时工作,C1获得更高的编译速度,C2获得更好的编译质量。

  • 第0层:程序解释执行,解释器不开启性能监控,可触发第1层编译。
  • 第1层:C1编译,简单、可靠的优化,必要时加入性能监控。
  • 第2层:启动一些编译耗时较长的优化,甚至根据性能监控信息进行不可靠的激进优化。

3. 编译对象和触发条件

热点代码

  1. 被多次调用的方法:整个方法为编译对象,虚拟机标准的JIT编译方式。
  2. 被多次执行的循环体。虽然循环在方法体内,但还是以整个方法为编译对象,这种编译方式为栈上替换,因为发生在方法执行过程中,方法帧还在栈上。

热点探测

  1. 基于采样的热点探测:虚拟机周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,说明是热点。优点:简单高效、容易获取方法调用关系。缺点:很难精确确认一个方法的热度,容易受到线程阻塞或别的干扰。
  2. 基于计数器的热点探测:虚拟机为每个方法甚至代码块建立计数器,统计方法执行次数。优点:精确。缺点:成本高。

HotSpot使用的是第二种方法。它有两类计数器:方法调用计数器和回边计数器。

方法调用计数器:统计方法被调用的次数,默认阈值Client下1500次,Server下10000次。

如果不做任何设置,方法调用计数器统计的不是方法调用的绝对次数,而是一段时间内的次数,超过一定时间后,计数器会减半。这种衰减实在GC时顺便进行的。可以用-XX:CounterDecay设置是否衰减,如果不衰减,那么随着时间的推移,总会达到次数进行编译。

回边计数器:统计一个方法中循环体执行的次数。准确的说是回边次数,空循环不会回边,只跳转到自己。
client模式阈值:方法调用计数器阈值OSR比率/100,OSR比率默认933。
server模式阈值:方法调用计数器阈值
(OSR比率 - 解释器监控比率)/100,OSR比率默认140,解释器监控比率默认33。

回边计数器没有热度衰减。

4. 一些编译技术

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

公共子表达式消除:如果一个表达式前面已经计算过了,后面表达式的变量也没有变化过,这个表达式就是公共子表达式,就没有必要计算了。

//javac不会作任何优化,但是JIT会优化b * c
int d = (c * b) * 12 + a + +(a + b * c)

数组边界检查消除:Java在访问数组元素时会自动进行上下界的范围检查,越界则抛出ArrayIndexOutOfBoundsException,但是这也是一种性能负担。虚拟机根据情况在编译器判断是否可能越界,如果不越界执行时就不需要检查了。
类似情况还有NullPointException,除数为0异常等。

方法内联:编译器最重要的优化手段之一,消除了方法调用的成本,还为其它优化建立了基础。

  • 方法内联不是代码复制那么简单,因为Java的方法(除了编译期解析的),编译期都不能确定版本,运行期才可以。采用“类型继承关系分析CHA”解决这一问题。

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

  1. 编译器进行内联时,如果是非虚方法,那么直接内联。如果遇到虚方法,则查询CHA是否有多个版本,如果只有一个,那么进行内联(激进的,需要逃生门)。如果后续虚拟机没有加载其它类改变继承关系,则一直内联,否则退回解释状态,或重新编译。
  2. 如果CHA查询出多个版本,编译器会使用内联缓存。在未发生调用前,缓存为空,发生调用后,缓存记录下方法版本信息,以后每次调用都比较版本,如果一直,内联继续,如果不一致,取消内联。查找虚方法表。

逃逸分析:分析对象的动态作用域。不是代码优化手段,而是为其它手段提供依据。

  • 方法逃逸:一个对象被外部方法引用,如传参。
  • 线程逃逸:对象被外部线程访问到,如赋值给类变量。

如果证明一个对象不会逃逸,那么可以进行一些高效的优化。

栈上分配:一般对象都在堆上分配,各个线程共享,GC回收内存需要耗费时间。如果一个对象确定不会逃逸出方法,比如局部变量,那么分配在栈上就很舒服,可以随栈帧出栈而销毁。GC压力减小。

同步消除:如果一个对象确定不是线程逃逸,那么就不会被其它线程访问,就不存在竞争,完全可以消除同步。

标量替换:如果一个对象确定不会被外部访问,那么真正执行的时候就不需要创建这个对象,改为在栈上创建对象拆散后的标量。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容