当前位置: 首页 > 建站技术 > 编程语言 > Java > 正文

  • 标签
  • 源码
  • 特效
  • Java 虚拟机 11 :运行期优化

    前言

    http://www.cnblogs.com/xrq730/p/4839245.html,HotSpot采用的是解释器+编译器并存的架构,之前的这篇文章里面已经讲过了,本文只是把即时编译器这块再讲得具体一点而已。当然,其实本文的内容也没多大意义,90%都是概念上的东西,对于实际开发、实际解决项目里面的疑难问题并没有什么太大的帮助,只要看过就好了。

    编译对象与触发条件

    之前讲过,Sun使用的虚拟机之所以被叫做”HotSpot”,就是因为运行过程中会检测热点代码,那么运行过程中,会被即时编译器编译的”热点代码”有两类,即:

    • 被多次调用的方法
    • 被多次执行的循环体

    前者很好理解,一个方法被调用得多了,方法体内代码执行的次数自然就多,他成为”热点代码”也是理所当然。而后者则是为了解决一个方法只被调用过一次或者少量的几次,但是方法体内部存在循环次数较多的循环体问题,这样循环体的代码也被重复执行多次,因此这些代码也应该认为是”热点代码”。

    那上面的问题描述中,所谓”多次”都不是一个具体、严谨的用语,那么多少次才算”多次”?还有,虚拟机如何统计一个方法或一段代码被执行过多少次呢?

    判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为”热点探测“,其实热点探测并不一定要知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种:

    • 基于采样的热点探测
    • 基于计数器的热点探测

    HotSpot虚拟机中使用的是第二种基于计数器的热点探测方法,它为每个方法准备了两类计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译,分别看一下:

    1、方法调用计数器

    顾名思义,这个计数器就是用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次。这个阈值可以通过参数-XX:CompileThreshold来人为设定。当一个方法被调用时,会检查方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器和回边计数器值之和是否超过方法调用计数器的阈值。如果已经超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

    如果这个参数不做任何设置,那么方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会少一半,这个过程称为方法的调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作实在虚拟机进行垃圾回收时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

    那如果参数不设置的话,执行引擎并不会同步等待编译请求完成,而是直接进入解释器按照解释方法执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的,下一次调用该方法时就会使用已编译的版本。

    2、回边计数器

    它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为”回边”。显然,建立回边技术其统计的目的就是为了触发OSR编译。关于回边计数器的阈值,虽然HotSpot也提供了一个类似于方法调用计数器阈值-XX:CompileThreshold的参数-XX:BackEdgeThreshold供用户设置,但是当前虚拟机实际上并未使用此参数,因此我们需要设置另外一个参数-XX:OnStackReplacePercentage来间接调整回边计数器的阈值,其计算公式如下:

    (1)Client模式

    方法调用计数器阈值?× OSR比率 / 1000,其中OSR比率默认值933,如果都取默认值,Client模式下回边计数器的阈值应该是13995

    (2)Server模式

    方法调用计数器阈值?× (OSR比率 – 解释器监控比率) / 100,其中OSR比率默认140,解释器监控比率默认33,如果都取默认值,Server模式下回边计数器阈值应该是10700

    当解释器遇到一条回边指令时,会先查找将要执行的代码片段中是否有已经编译好的版本,如果有,它将会优先执行已编译好的代码,否则就把回边计时器的值加1,然后判断方法调用计数器与回边计数器值之和是否已经超过回边计数器的阈值。当超过阈值之后,将会提交一个OSR编译请求,并且把回边计数器的值降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果。

    与方法计数器不同,回边计数器没有热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程。

    编译过程

    很简单过一下这块编译过程的内容,因为这主要是编译原理和代码优化中的内容。

    在默认设置下,无论是方法调用产生的即时编译请求,还是OSR编译请求,虚拟机在代码编译器还未完成的时候,都仍然按照解释方式继续执行,而编译动作则在后台的编译线程中进行。用户可以通过-XX:-BackgroundCompilation来禁止后台编译,在禁止后台编译后,一旦达到JIT的编译条件,执行线程向虚拟机提交编译请求后将会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。

    对于Client Compiler(C1编译器)来说,它是一个简单快速的三段式编译,主要关注点在于局部性的优化,而放弃了许多耗时间长的全局优化手段。

    对于Sever Compiler(C2编译器)来说,它则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高编译器,几乎能达到GNU C++编译器使用-O2参数时的优化强度,它会执行所有经典的优化动作,如无用代码消除、循环展开、常量传播、基本块重排序等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除等,另外,还有可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联、分支频率预测等,下一部分将讲解上述的一部分优化手段。

    Server Compiler从即时编译的标准来看,无疑是比较缓慢的,但它的编译速度依然远远超过传统的静态优化编译器,而且它相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server模式的虚拟机运行。

    优化技术

    在Sun官方的Wiki上,HotSpot虚拟机设计团队列出了一个相对比较全面、在即时编译器中采用的优化技术列表,其中有不少经典编译器的优化手段,也有许多针对Java语言(准确地说是运行在Java虚拟机上得所有语言)本身继续拧的优化技术,下面主要看几项有代表性的优化技术:

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

    1、公共子表达式消除

    公共子表达式消除消除的含义是:如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再去对它进行计算,只需要直接用前面计算过的表达式结果替代E就可以了。如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除;如果这种优化的范围涵盖了多个基本块,便称为全局公共子表达式消除。举个简单的例子,假设存在以下代码:

    int d = (c * b) * 12 + a + (a + b * c);

    如果这段代码交给Javac编译器则不会进行任何优化。但是这段代码进入到虚拟机即时编译器之后,它将会进行如下优化,编译器检测到”c * b”和”b * c”是一样的表达式,而且在计算期间b与c的值是不变的,因此这条表达式将被视作:

    int d = E * 12 + a + (a * E);

    这时,编译器还可能进行另一种叫做代数简化的优化,把表达式变为:

    int d = E * 13 + a * 2;

    表达式进行变换之后,在计算起来就可以节省一些时间了

    2、数组范围检查消除

    我们知道Java语言是一门动态安全的语言,对数组的读写访问也不像C、C++那样在本质上是裸指针操作,如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候将会自动进行上下界的范围检查,即检查i>=0&&i<foo.length这个条件,否则将会抛出一个数组下标越界异常。这对开发者来说是一件好事,即使程序员没有专门编写防御代码,也可以避免大部分的溢出攻击,但是对于虚拟机来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,无疑也是一种性能负担。

    无论如何,为了安全,数组边界检查肯定是必须做的,但数组边界检查是不是必须在运行期间一次不漏地检查则是可以商量的。比如数组下标是一个常量,只要在编译期间根据数据流分析来确定foo.length的值,并判断下标有

    关注创业、电商、站长,扫描方便乐网站微信二维码,定期抽大奖。

    【版权与免责声明】如发现内容存在版权问题,烦请提供相关信息发邮件至2723741405@qq.com,我们将及时沟通与处理。本站内容除非来源注明方便乐,否则均为网友转载,涉及言论、版权与本站无关。

    本文永久链接:http://www.fangbianle.com/news/show-254652.html

  • 营销
  • 创业
  • 电商
  • 微商
  • 优秀营销策划:有机会我们就抓住机会,没有机会我们就创造机会
    那些关于抽奖你不知道的事,其实吸粉也得靠套路!
    内容运营:如何借助热点,让你的产品一炮走红
    营销策划的流程
    万商汇:移动营销从粗放走向精细化
    好活动才得人心,可是什么样的活动才是好活动呢?
    如何才能将社群成员转化为客户呢?提升转化率的几点小妙招奉上
    营销策划功效