后端编译与优化

编译器优化技术

逃逸分析

逃逸分析本身不是直接进行优化的方法,而是一种为优化提供帮助的分析算法

逃逸分析的原理:

​ 我们分析对象的动态作用域,如果对象创建在方法中,可能被外部方法所引用到,例如作为参数被外部方法进行调用,这叫做方法逃逸

​ 被外部线程所访问到,例如赋值给外部线程使用的变量中。称为线程逃逸

​ 从不逃逸,方法逃逸,线程逃逸。称为对象从小到大的逃逸程度,我们因此也可以做不同的优化策略、

栈上分配

​ 大家都知道在java中,所有的对象都存储在堆空间中,随着存放 的对象越来越多,也就需要垃圾回收器来进行工作,而这一步也是非常消耗性能的,那这里就引出一个概念栈上分配,也就是当我们进行逃逸分析后,发现对象不存在线程逃逸,我们将对象存储在栈上,随着栈帧的插入与弹出,对象本身也跟着创建和销毁。

这里我们需要提醒两点:

  1. 之前放在堆中,堆中对象是被各个线程共享的,只要有指针指向该对象的地址,就可以进行使用。但栈是线程独有的。所以栈上分配的第一点:对象不会被线程共享,也叫做线程逃逸

  2. 如果对象被分配到栈上了,随着栈帧插入与弹出(也就是方法的调用与结束),变量会跟着创建和删除,那如果该变量被方法外所引用,比如被当做方法参数被其他方法进行调用。这也称为方法逃逸

​ 栈上分配支持:方法逃逸,不支持:线程逃逸

标量替换

这里的标量是指Java中基础变量,例如:int,char,boolen,short,Refference等,也就是变量不能再拆分为更小的元素,这就叫标量

而当我们创建对象后,对象可以再被细分为各种标量的组合,该对象也叫做聚合量

​ 面向过程编程的一个好处就是不用封装对象,效率更高,所以通过这一点,如果我们能分析出该对象只有一部分会被使用,且对象不会逃逸到方法体外,则我们不创建该对象,而是直接创建对象的标量,进行使用。下面我们来代码解释一下:

class Person{
  int x;
  int y;
  public Person(int x,int y) {
    this.x = x;
    this.y = y;
  }
  public int getX() {
    return x;
  }
}
//没有优化前
public int getValue(int x) {
  int XX = x+2;
  Person person = new Person(XX,42);
  return person.getX();
}
//第一步:构造函数内联后的样子
public int getValue(int x) {
  int XX = x+2;
  Person p = point_memory_alloc();  //堆中分配P对象
  p.x==XX;													//Person构造函数内联后
  p.y==42;
  return p.x;												//Person::getX()被内联后
}

//第二步:进行标量替换优化
public int getValue(int x) {
  int XX = x+2;
	//相当于我不创建一个完整对象了,我只创建几个标量来代替使用
  int pX = XX;
  int py = 42;
  return pX;
}
//第三步:作无效代码消除后
public int getValue(int x) {
  return x+2;
}

​ 标量替换会更加严格一些:不允许方法逃狱。

同步消除

同步消除是指Java中的锁的优化,也就是当我们分析出一个同步的方法,实际中不会被其他线程所争抢,那么久没有必要上锁了,相当于你去公共厕所,怕别人进来,你上把锁。走了再开锁,那如果是在你家,只有你一个人时,就没必要再加个锁吧,再懒点儿你门都可以不要了。

​ 当经过逃逸分析后,我们发现方法不会被其他线程访问使用,也就是线程逃逸,我们就可以不对线程进行同步操作。

小总结

​ 从测试中我们发现效果不错,但实际中可能分析后消耗了性能还发现能被优化的很少。所以在JDK 6 Update 23之前是禁止该优化的,之后才开始默认开启逃逸分析。

  1. -XX:+DoEscapeAnalysis手动开启逃逸分析
  2. -XX:+PrintEscapeAnalysis来查看开启后的分析结果
  3. -XX:+EliminateAllocations开启标量替换
  4. -XX:+PrintEliminateAllocations查看标量替换结果
  5. -XX:+EliminateLocks开启同步消除