运行期优化

一、概述

即时编译器(Just In Time Compiler,JIT编译器):Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行
特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关
的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器。

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

1、解释器与编译器

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译时间立即执行。
在程序运行后,随时间推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以提供执行效率。

HotSpot虚拟机中内置两个即时编译器,分别称为Client Compiler 和Server Compiler,简称C1编译器和C2编译器。
默认C1编译器,可以使用-client 或 -server 指定。
解释器与编译器搭配使用的方式在虚拟机中称为混合模式,用户可以通过参数-Xint强制虚拟机运行与解释模式(Interpreted Mode),
这时编译器完全不参与工作,全部代码都使用解释方式执行。
也可以使用-Xcomp强制虚拟机运行编译模式(Compiled Mode),这时将优先采用编译方式执行程序。
Client Compiler 可以获得更高的编译速度,Server Compiler可以获得更好的编译质量。

2、编译对象与触发条件u

热点代码:

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

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

  • 基于采样的热点探测:虚拟机周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法。
  • 基于计数器的热点探测:虚拟机为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定阈值,就认为是热点方法。
    HotSpot采样的是基于计数器的热点探测,为每个方法准备两类计数器:方法调用计数器、回边计数器。

方法调用计数器:统计方法被调用的次数,Client模式下默认1500次,Server模式下默认10000次,可以通过虚拟机参数-XX:CompiledThreshold修改。
回边计数器:统计一个方法中循环体代码执行次数。

三、编译优化技术

1、公共子表达式消除

如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有的变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。
对于这种表达式就没有必要花时间对它进行计算,只需直接用前面计算过的表达式结果代替E就可以了。

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

当这段代码进入虚拟机即时编译器编译后,就变为了第二行代码

2、数组边界检查消除

java 在访问数组类型数据时会自动进行上下界范围检查,如果超出范围将抛出ArrayIndexOutOfBoundsException,每次数据读写都带有一次
隐含的条件判定,影响性能。对于循环体来说,如果编译器可以判定循环变量取值范围永远在[0,length())之间,那在整个循环中就可以
把数组上下界检查消除。

3、方法内联

把目标的代码复制到发起调用的方法中,避免发生真实的方法调用。
分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
如果能证明一个对象不会逃逸到变量或线程外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为为这个变量进行优化。

  • 栈上分配:java虚拟机中,在java堆上分配创建对象的内存空间,java堆中的对象对于各个线程都是共享可见的,只要持有这个对象的引用,
    就可以访问堆中存储的对象数据。虚拟机垃圾收集系统可以回收堆中不再使用的对象。

如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配,对象占用的内存就可以随栈帧出栈而销毁。

  • 同步消除:线程同步本身就是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写
    肯定就不会有竞争,对这个变身实施同步措施也就可以消除。
  • 标量替换:标量是指一个数据已经无法分解成更小的数据来表示,Java虚拟机中的原始数据类型都不能再进一步分解,就可以称为标量。
    相对的,如果一个数据可以继续分解,称为聚合量,java中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其使用

到的成员变量恢复原始类型来访问就叫标量替换。如果逃逸分析证明一个对象不会被外部访问,并且对象可以被拆散的话, 程序真正执行的时候
可能不创建这个对象,而改为直接创建它的若干被这个方法使用到的成员变量替换。这些变量建立在栈上分配读写。

上一篇:python用WMI等获取及修改windows系统信息


下一篇:java并发编程实战 第六章 任务执行