JVM学习--(七)后端编译与优化

JVM学习–(七)后端编译与优化

后端主要指的是将Class文件转化成与本地基础设施相关的二进制机器码的过程。与普通的解释器相比,提前编译器和即时编译器的加入优化了后端的过程,虽然两者都不是一个虚拟机所必备的,确实一个虚拟机好坏的重要衡量标准之一。

即时编译器

java虚拟机是通过解释执行的,当虚拟机发现某个方法或者代码块运行的特别频繁,就会把这些代码认定为“热点代码”,就会在运行时把这些代码转成本地机器码,并用各种手段来优化代码,完成这个任务的编译器被称为即时编译器,本节主要通过对于5个问题的问答来展开:
1️⃣为什么HotSpot中要采取解释器和即时编译器共存的架构?
答:并不是所有的虚拟机都是共存的架构,但是基本上的主流虚拟机都采用了共存的架构模式。主要是因为解释器和编译器两者各有优势:①当程序需要快速启动时,解释器可以省去编译的时间,直接执行。当程序启动后,随着时间的推移,编译器逐渐发挥作用,随着本地代码的增多,可以获得更高的执行效率。
②程序运行过程中内存限制较大时,可以使用解释执行节约内存,反之可以通过编译器来提高效率。
③解释器可以作为编译器激进优化时后备的“逃生门”。
因此解释器和编译器相辅相成,需要相互配合工作。
2️⃣为什么HotSpot虚拟机要实现两个(三个)不同的即时编译器?
其中的两个即时编译器是存在已久了,分别是“客户端编译器”和“服务端编译器”,简称C1和C2。第三个是JDK10出现的,长期目标是为了取代C2的Graal编译器。
3️⃣程序何时使用解释器?何时使用编译器?
JDK6之前,分层编译功能还没有实现,选择哪个编译器完全取决于虚拟机运行的模式。JDK7后根据编译器编译、优化的规模和耗时,划分出不同的编译层次,其中包括:
第零层:纯解释执行,并且解释器不开启性能监控功能。
第一层:使用客户端编译器进行优化,但是不开启性能监控功能。
第二层:仍然使用客户端编译器,开启方法及回边次数统计等少量性能监控功能。
第三层:仍然使用客户端编译器执行,开启所有的性能监控功能。
第四层:使用服务端编译器,并且根据性能监控信息进行一些不可靠的激进优化。
实现多层编译功能后,解释器以及C1C2编译器会同时工作。
4️⃣哪些程序代码会被编译成本地代码?如何编译本地代码?
会被即时编译器编译的是“热点代码”,这里的热点代码有两类,包括:
1.被多次调用的方法。2.被多次执行的循环体。
编译的目标对象都是整个方法体,而不会是单个的循环体。对于后面一种,尽管编译动作是由循环体所触发的,热点只是方法的一部分,执行入口会稍有不同,编译时会传入执行入口点字节码序号。这种编译方法因为编译发生在方法执行的过程中,因为被很形象地称为“栈上替换”。
对于热点代码的探测主要有两种:1.基于采样的热点探测。2.基于计数器的热点探测。
HotSpot采用的是第二种的探测方式:对于方法的的计数有热度的衰减,这段时间被称为方法统计的半衰期,对于用来循环体计数的回边计数器没有半衰期,在字节码中遇到控制流向后跳转的指令就称为“回边”。

编译过程

客户端编译器和服务端编译器的编译过程是有区别的,对于客户端编译器而言,它是一个简单的三段式编译器,主要的关注点是在于局部性的优化,而放弃了很多耗时较长的全局优化手段。
第一阶段:平台独立的前端将字节码转成HIR,HIR使用了静态单分配的形式来代表代码值,方法内联等可以在这过程中完成。
第二阶段:平台的后端将HIR转成LIR,在转化过程前就是将HIR转成优化后的HIR会完成空值检查消除和范围检查消除等方法。
最后阶段:平台的后端使用线性扫描算法在LIR分配寄存器,例如寄存器分配等生成机器代码。
以上就是客户端编译器的三段优化,服务器端的配置更加复杂一点,服务器编译采用的寄存器是一个全局图着色分配器,可以充分利用某些处理器架构的大寄存器集合。

提前编译器

主要的提前编译有两个分支:
1.在程序运行前把程序代码编译成机器码的静态翻译工作。
2.把原来即时编译器在运行时要做的编译工作提前做好保存下来,下次运行到直接加载进来。
优劣性:
1.即时编译最大的弱点是需要占用程序运行时间和运算资源。
2.即时编译器对于提前编译器的三个天然优势:
1️⃣性能分析制导优化:即时编译器在运行过程中通过性能监控功能会不断收集信息,这些信息是静态分析无法得到的,可以通过这些信息通过即时编译器集中处理。
2️⃣激进预测性优化:可以通过信息收集做些大胆的优化尝试,就算失败还可以用解释器继续运行,但是提前编译器不能做这样得操作。
3️⃣链接时优化:java语言天生是动态链接的,只有在运行过程中才可以在即时编译器中生成优化后的本地代码。

编译器优化技术

主要介绍四种:1.最终的技术优化:方法内联 2.最前沿的优化技术:逃逸分析 3.语言无关的经典优化技术:公共子表达式消除 4.语言相关的经典优化技术:数组边界检查消除
1️⃣方法内联:
方法内联就是把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免真实的方法调用。
对于java方法来说,难点在于很多方法是虚方法,在运行前不知道调用的多态选择,为了解决这个问题,java虚拟机引入了一个名为类型继承关系分析(CHA),编译器会根据不同的情况采用不同的方法:
①如果是非虚的方法,直接进行内联即可。
②如果是虚方法且这个方法在当前程序状态下只有一个目标版本可以选择,可以通过假设进行“守护内联”。因为在后面可能加载到了新的类型会改变CHA结论,所以这种内联属于激进预测性优化,必须预留好“逃生门”。
③如果是虚方法且有多个版本可以选择,将用“内联缓存”的方式来缩减方法调用的开销,可以理解为记录下每次不同版本的方法调用,调用一次后下一次只要判断方法所采取的是什么版本就可以立刻进行内联。
2️⃣逃逸分析:
这并不是直接优化代码的手段,只是为其他优化措施提供依据的分析技术。
当一个对象在方法里面被定义后,它可能被外部方法所引用,这种被称为方法逃逸,甚至被外部线程访问到,这称为线程逃逸。从不逃逸到方法逃逸再到线程逃逸被称为对象由低到高的不同逃逸程度。
如果一个对象不会逃逸或者逃逸几率极低,则可以做以下的优化:
①栈上分配:让这个对象不在java堆中分配内存,直接在栈上分配内存,对象所占用的内存空间可以随着栈帧出栈而销毁。
②标量替换:一个数据无法分解成更小的数据来表示,那么这就是标量,否则为聚合量。对象是典型的聚合量,如果这个对象不会逃逸,可以将它拆散,根据程序访问情况,将其成员变量恢复为原始类型来访问。
③同步消除:不会逃逸就不会被其他的线程所访问,因此可以将这个变量的同步措施都取消。
3️⃣公共子表达式消除:
对于已经计算过的变量,在后续调用中可以调用结果,而将变量的表达式删去。
如果这种优化仅仅局限于程序基本块中,可以称为局部公共子表达式消除;优化的范围涵盖了多个基本块,可以称为全局公共子表达式消除。
4️⃣数据边界检查消除
java是一个动态安全语言,每次访问数据前需要检查上下界,但是每次检查需要浪费掉很多运行时间,因此会采用隐式异常处理,只有发生错误时才会进行检查。

你可能感兴趣的:(JVM)