晚期(运行期)优化(笔记)

一、概述

即时编译器性能的好坏、代码优化程度的高低是衡量一款商用虚拟机优秀与否的最关键指标之一。

二、HotSpot虚拟机内的即时编译器

1、解释器与编译器

1.1.许多主流的商用虚拟机都采用解释器与编译器并存的架构。

1.2.优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的世界,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

1.3.使用场景:当程序运行环境中内存资源限制较大,(如部分嵌入式系统),可以使用解释器执行节约内存,反之可以使用编译执行来提高效率。

1.4.“逃生门”:当编译器采用的激进优化的假设不成立时,解释器作为一个“逃生门”,将程序通过逆优化退回到解释状态继续执行。

1.5.两个即时编译器:Client Compiler(C1编译器)、Server Compiler(C2编译器)。程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式。用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

1.6.混合模式:无论是Client Compiler还是Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”。虚拟机默认采用混合模式。虚拟机默认使用混合模式,我们可以通过以下的参数进行设置。

1.7.分层编译

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

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

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

实施分层优化后,Client Compiler和Server Compiler 将会同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler获取更好的编译质量。

2、编译对象与触发条件

2.1.“热点代码”:A、被多次调用的方法;B、被多次执行的循环体

2.2.编译对象:整个方法。对于两种热点代码,编译器都会以整个方法作为编译对象。而后一种热点代码因为编译发生在方法执行过程中,一次形象地称之为栈上替换(On Stack Replacement,OSR,即方法栈帧还在栈上,方法就被替换了)

2.3.热点探测

#基于采样的热点探测:周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这个方法就是“热点方法”。

优点:实现简单、高效。还可以很容易地获取方法调用关系。

缺点:很难精确地确认一个方法的热度。容易受到线程阻塞(导致某些方法一直在栈顶)或别的外界因素影响而扰乱热点探测。

#基于计数器的热点探测:为每个方法(甚至是代码块)建立计数器,统计方法的执行次数。

优点:统计结果相对来说更加精确和严谨

缺点:实现起来较麻烦,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系。

2.4.HotSpot——采用第二种

两类计数器:方法调用计数器和回边计数器。当这两个计数器超过阈值溢出了,就会触发JIT编译。

#方法调用计数器——统计方法被调用的次数。

当一个方法被调用时,会先检查方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则此方法的调用计数器加1,然后判断方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值。如果超过,那么将向即时编译器提供一个该方法的代码编译请求。如果不做任何设置,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译完成后,这个方法的调用入口地址就会被系统自动改写成信的,下一次调用该方法时就会使用已编译的版本。过程如下:

计数器热度的衰减:如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,如果方法的使用次数仍然不足以让它提交给即时编译器,那这个方法的调用计数器就会被减少一半。这段时间称为方法的半衰周期。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的。

#回边计数器——统计一个方法中循环体代码执行的次数

在字节码中遇到控制流向后跳转的指令称为“回边”。显然,建立回边计数器的目的就是为了触发OSR编译。

回边计数器阈值计算公式:

#Client模式下:方法调用计数器阈值*OSR比率/100

#Server模式下:方法调用计数器阈值*(OSR比率-解释器监控比率)/100

整个过程和上面的方法调用计数器差不多。

区别:回边计数器没有热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出时,它还会把方法计数器的值也调整到溢出状态,这样下次进入该方法的时候就会执行标准编译过程。以上所讲的都是Client模式下的即时编译方式,Server模式下会更复杂。

3、编译过程

无论是方法调用产生的即时编译请求,还是OSR编译请求,在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译栈中进行。可通过参数设置来禁止后台编译,执行线程将会一直等待,知道编译过程完成后再开始执行编译器输出的本地代码。

3.1.Client Compiler——关注点在于局部性的优化,放弃了很多耗时较长的全局优化手段

简单快速的三段式编译器

#第一个阶段:一个平台独立的前端将字节码构造成了一种高级中间代码表示(HIR)。HIR使用静态单分配的形式来表示代码值,在此之前编译器会在字节码的基础上完成一些优化,如:方法内联、常量传播等。

#第二个阶段:一个平台相关的后端从HIR中产生低级中间代码表示(LIR)。在此之前会在HIR的基础完成一些优化,比如空值检查消除、范围检查消除等。

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

3.2.Server Compiler

专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器。几乎能达到GNU C++编译器使用-O2参数时的优化强度,它会执行所有经典的优化动作。

性能:Server Compiler上的寄存器是一个全局着色分配器,它可以充分利用某些处理器架构上的大寄存器集合。以即时编译的标准来看,Server Compiler无疑是比较缓慢的,但它的编译速度依然远远超过传统的静态优化编译器,而且它对Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server模式的虚拟机运行。

三、编译优化技术

Java程序员有一个共识,以编译方式执行本地代码比解释执行更快。原因除了虚拟机解释字节码时额外消耗时间外,还有一个很重要的原因就是虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器中。

接下来,我们来查看几项最优代表性的优化技术是如何运作的:

#语言无关的经典优化技术之一:公共子表达式消除

#语言相关的经典优化技术之一:数组范围检查消除

#最重要的优化技术之一:方法内联

#最前沿的优化技术之一:逃逸分析

1、公共子表达式消除

1.1.含义:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共表达式。不必要再计算,直接用之前计算过的表达式结果代替E就可以了。

1.2.分类

#局部公共子表达式消除:优化仅限于程序的基本块内

#全局公共子表达式消除:优化涵盖了多个基本块

2、数组边界检查消除

每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑也是一种性能负担。

2.1.解决思路:

#把数组边界检查优化尽可能从运行期提到编译器完成

#隐式异常处理

if(foo!=null){
  return foo.value;
}else{
  throw new NullPointException;
}

在使用隐式异常优化后,虚拟机会把上面伪代码所表示的访问过程变成如下伪代码:

try{
  return foo.value;
}catch(segment_fault){
  uncommon_trap();
}

虚拟机会注册一个Segment Fault信号的异常处理器,这样当foo不为空的时候,对value的访问时不会额外消耗一次对foo判空的开销的。代价就是当foo真为空时,必须转入到异常处理器中恢复并抛出NullPointException异常,这个过程必须从用户态转到内核态中处理,结束后再转回到用户态,速度远比一次判空检查慢。当foo极少为空时,隐式异常优化是值得的。还好HotSpot虚拟机可以根据运行期收集到的Profile信息自动选择最优方案。

3、方法内联

3.1.作用:除了消除方法调用的成本以外,它更重要的意义是为其他的优化手段建立良好的基础。

3.2.代码清单

public static void foo(Object obj){
  if(obj!=null){
    System.out,println("do");
  }
}

public static void main(String[] args){
  Object obj=null;
  foo(obj);
}

事实上main方法中全部都是无用的代码,如果不做内联,后续即使进行了无用代码消除的优化,也无法发现任何“Dead Code”,因为如果分开来看,foo()和main()两个方法里面都可能是有意义的。

3.3.内联判断

除了非虚方法是在编译器解析以外,其它的Java方法(虚方法)都有可能存在多于一个版本的方法接受者。对于一个虚方法,编译器做内联的时候根本无法确定应该使用哪个方法版本。例如,有ParentB和SubB两个类,并且子类继承了父类的get()方法,那么到底是要执行父类的get()方法还是子类的get()方法,需要在运行期才知道,编译器无法得出结论。

3.4.类型继承关系分析

内容:这是一种基于整个应用程序的类型分析技术,它用于确定在目前已知加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。内联过程如下:

“逃生门”:虚拟机如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译。

内联缓存:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接受者的版本信息,并且每次进行方法调用时都比较接受者版本,如果以后进来的每次方法调用的方法接受者版本都是一样的,那这个内联还可以一直用下去。如果发生了方法接受者不一致,就说明程序真正使用了虚方法的多态特性,这时才会取消内联,查找虚方法表进行分派。

4、逃逸分析

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

高效优化:

——前提是能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象。

#栈上分配:Java虚拟机中,在Java堆上分配创建对象的内存空间几乎是Java程序员都清楚的常识了。但是如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个不错的注意,对象所占用的内存空间就可以随栈帧出栈而销毁。垃圾收集系统的压力将会减小很多。

#同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步消除措施也就可以消除掉。

#标量替换:标量是指一个数据已经无法再分解成更小的数据来表示了。相对的,可以继续分解的叫聚合量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。

四、Java与C/C++的编译器对比

——实际上这两者的对比代表了最经典的即时编译器与静态编译器的对比。“拼编译器”和“拼代码输出质量”

1、Java的劣势

#即时编译器占用的是用户程序的运行时间,使得它不敢随便引入大规模的优化技术

#Java是动态的类型安全语言,这意味着需要由虚拟机来保证程序不会违反语言语义或访问非结构化内存。也就是虚拟机必须频繁地进行动态检查,需要消耗不少时间

#使用虚方法的频率远远大于C/C++,意味着虚拟机在进行一些优化时(方法内联)的难道要远大于静态编译器

#Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,使得很多全局优化都难以进行

#Java语言中对象的内存都是堆上分配,只有方法中的局部变量才能在栈上分配。并且C/C++主要由用户程序代码来回收分配的内存,这就不存在无用对象筛选的过程,效率也会比垃圾收集机制高

2、优势

上面所说的Java的劣势都是为了换取开发效率上的优势,动态安全、动态扩展、垃圾回收等特性都为Java语言的开发效率做出了很大贡献。

Java的另一个红利是由它的动态性带来的,由于C/C++编译器所有的优化都在编译器完成,以运行期性能监控为基础的优化它都无法进行,而这些都会成为Java语言的性能优势。

 

博客内容来自《深入理解Java虚拟机》。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值