深入理解java虚拟机(19):编译优化技术

 

 看如下代码

package org.xiaofeiyang.classloader;

/**
* @author: yangchun
* @description:
* @date: Created in 2019-12-02 17:03
*/
public class B {
static class A{
int value;
final int get(){
return value;
}
}
public void foo(){
A a=new A();
int y=a.get();
int z=a.get();
int sum = y+z;
}
}
代码优化一定是建立在中间码或者机器码的层面,第一步进行方法内联,有两个目的。第一个目的就是去掉方法调用成本,第二就是为其他优化建立良好的基础。第二步就是进行冗余访问消除,y,z的值都没有改变,就可以不用访问局部变量a
公共子表达式优化就会int z=y。第三步进行复写传播,这一个代码中没有必要用到z变量,直接y=y就行。第四部消除无用代码就可以,y=y没有意义,因此sum=y+y就可以了。
以上四步主要使用了如下技术
1)公共子表达式消除
公共子表达式消除是一个普遍应用于各种编译器的经典优化技术,它的含义是:如果是一个表达式E已经被计算过,并且从先前的计算到现在E中所有的变量值都没有发生变化,E就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行
计算,如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除,如果这种优化范围涵盖了多个基本模块,那就称为全局公共子表达式消除。
int d=(b*c)*12+a+(a+b*c),未优化字节码如下

 这段代码进入虚拟机即时编译器后,编译器检查到b*c,c*b是一样的表达式而且计算期间b与c的值只是不变的,因此,这条表达式就可被视为:

扫描二维码关注公众号,回复: 8051995 查看本文章

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


2)数组范围检查消除
java每次数组的读写,对于大量数组操作访问的程序代码,性能开销很严重。这种运行期检查提到编译期完成的思路外,另外还有一种避免思路-隐式的异常处理如下代码
if(foo!=null){
  return foo.value
}else{
  throw new NullPointException()
}
虚拟机处理上面代码会注册一个SegmentFault信号的异常处理,这种情况不会增加异常foo判断空的额外开销,代价是如果真为空,必须从用户态转入内核态结束后再转换成用户态。当foo极少为空的情况下可以这么操作,虚拟机足够聪明
它会根据运行期收集到的Profile信息自动选择最优方案。
try{
  return foo.value
}catch(segment_fault){
  uncommon_trap()
}
3)方法内联
java方法解析和分派调用时候就介绍过。只有使用invokespecial指令调用的私有方法,实例构造器,父类方法,以及使用的invokestatic指令进行调用的静态方法才是在编译期进行解析。其余的方法java调用都需要在运行时方法接受者的
多态选择,简而言之java实例调用的方法默认是虚方法。为了解决虚方法内联问题,java虚拟机团队想出来了一个类型继承关系分析技术。这是一种基于整个应用程序的类型分析技术,它用于确定目前加载的类中,某个接口是否有多于一种的实现,
某个类是否存在子类,子类是否为抽象类的信息。如果遇到虚方法会向CHA查询此方法在当前版本有多少目标可以选择,如果只有一个直接进行内联,这个属于激进优化,要预留一个逃生门。如果没有加载指令可以导致继承关系发生变化的新类,
那就需要抛弃已经编译的代码,退回到解释执行的状态或者重新编译。如果有多个版本可以提供选择,使用内联缓存这是建立在目标方法正常入口之前的缓存,第一次调用缓存为空,第一次调用缓存记录下方法接收者的版本信息,并且每次进行调用
时都比较接收者版本,如果以后进来的每次方法调用接收者一致就可以走缓存,那这个内联就可以一直用下去,如果发生了方法接收者不一致的情况,就说明程序真的使用虚方法的特性,查找虚方法表进行分派,并且取消内联。
4)逃逸分析
分析对象动态作用域,当一个对象被定义后,它可能被外部方法引用。作为参数传递给外部,被称为方法逃逸,被外部线程访问到,赋值给类变量或其他线程中可以访问的变量实例被称为线程逃逸。如果一个对象不能逃逸,则可能为这个变量进行一些高效的优化
虚拟机回收机制可以回收堆对象,不可以回收栈对象,整理和回收内存都需要时间。如果确定一个对象不会逃逸,那让这个对象在栈上分配内存,对象所占空间也会随着栈帧出栈而销毁。同步消除,不能逃逸对象不需要进行同步。标量替换标量是指
一个数据已经无法分解成更小的数据表示,java虚拟机的原始数据类型。如果一个对象拆散,根据程序访问的情况,将其使用的成员变量都变成原始类型来访问,叫做标量替换。如果逃逸分析一个对象不会被外部访问到,并且可以拆分,程序真正执行的
时候将可能不会创建这个对象,改为直接创建它的若干被这个方法使用到的成员变量来替代。将对象拆分拆分后除了变量可以直接在栈上创建以外,有很大概率会被虚拟机分配到高速寄存器中存储和读写,还可以为下一步优化手段创建条件。目前
虚拟机只能使用时间压力相对比较小的来完成逃逸分析。如果有需要-XX:+DoEscapeAnalysis开启逃逸分析,参数-XX:+PrintEscapeAnalysis查看分析结果。使用-XX:+EliminateAllocations开启标量替换,-XX:+EliminateLocks开启
同步消除,-XX:+PrintEliminateAllocations查看标量替换情况。


猜你喜欢

转载自www.cnblogs.com/xiaofeiyang/p/11973256.html