第十一章 晚期(运行期)优化 《深入理解java虚拟机》

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/xuchuanliang11/article/details/102748237

当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码。在运行时,虚拟机将会把这些代码编译成本地平台相关机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)

解释器(Interperter)和编译器并存:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。

HotSpot虚拟机中内置了两个即时编译器,分别是Client Compiler和Server Compiler,简称C1和C2

HotSpot还会逐渐启用分层编译,分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:

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

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

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

用Client Compiler获取更高的编译速度,用Server Compiler来获取更好的编译质量。

热点代码有两类:被多次调用的方法、被多次执行的循环体。

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

1.基于采样的热点探测:虚拟机周期性检查栈顶,如果某个方法经常出现在栈顶,则判定该方法是热点方法。优势是实现简单、高效,可以很容易获取方法调用关系,缺点是难精准地确认一个方法热度。

2.基于计数器的热点探测:为每个方法建立计数器,统计方法的执行次数,如果执行次数超过阈值则认定是热点方法。缺点是麻烦,优点是准确和严谨。

HotSpot使用基于计数器的热点探测,为每个方法准备两类计数器:方法调用计数器和回边计数器。两个计数器都有确定的阈值,超过阈值则会触发JIT编译。

方法调用计数器:默认C1是1500次,C2是10000次。通过-XX:CompileThreshould设定。

默认方法调用计数器并不是方法被调用绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定时间限度,如果方法调用次数仍然不足以让它提交给即时编译器,那这个方法的调用计数器就会被减少一半,这个过程称为衰减。

JIT编译过程

第一个阶段:一个平台独立的前端将字节码构造成一种高级中间代码表示;

第二个阶段:一个平台相关的后端从HIR中产生低级中间代码表示;

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

编译器优化:

第一步进行方法内联,方法内联的主要目的有两个,一是去除方法调用的成本,二是为其他优化建立良好的基础,方法内联膨胀之后便于在更大范围上采取后序优化手段,从而获取更好的优化效果。

第二步进行冗余访问消除

第三步进行复写传播

第四步进行无用代码消除

方法内联:java方法解析和分派调用中,只有进行invokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令进行调用的静态方法才是在编译期进行解析的,除了前面4中方法外,其他的java方法调用都需要再运行时进行方法接收者的多态选择,并且都有可能存在多于一个版本的方法接受者,所以java语言中默认的实例方法是虚方法。

对于一个虚方法,编译期做内联的时候根本无法确定应该使用哪个方法版本,为了解决虚方法内联问题,首先 引入一种名为类型继承关系分析(CHA)的技术,这是一种基于整个应用程序的类型分析技术,它用于确定在目前以加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。

编译器在进行内联时,如果是非虚方法,那么直接进行内联。如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,也可以进行内联。如果程序在后续执行过程中,虚拟机一直没有加载到会令这个方法接收者的继承关系发生变化的新类,那么则这个内联继续使用下去,否则抛弃已经编译的代码,退回到解释状态执行,或者重新编译。

如果向CHA查询出来的结果是多个版本的目标方法可供选择,则编译器使用内联缓存来完成内联,在未发生方法调用之前,内联缓存状态为空,当第一次发生调用后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都会比较接收者版本,如果版本发生变化则会取消内联,查找虚方法进行分派,否则继续内联。

猜你喜欢

转载自blog.csdn.net/xuchuanliang11/article/details/102748237