编译器优化的两大措施:标量替换、栈上分配。这两者都是基于逃逸分析去做的
逃逸分析
就是分析变量能否套出他的作用域,可以细分为四种场景:
- 全局变量赋值逃逸
- 方法返回值逃逸
- 实例引用逃逸
- 线程逃逸
四种场景代码举例:
// 测试变量逃逸分析
public class EscapeTest {
public static OtherClass inlineTest;
//全局变量赋值逃逸
public void globaVariable() {
/**
* 在方法这里,把一个局部变量new InlineTest() 赋值给了一个静态变量 inlineTest ,
* 局部变量的作用域应该是在方法内部的,类变量的作用域是在类里面,new InlineTest() 的作用域被放大了,发生了逃逸
*/
inlineTest = new OtherClass();
}
// 方法返回值逃逸
public OtherClass methodPointerEscape(){
/**
* new InlineTest() 的作用域是在方法内部的。但是作为返回值返回了,作用域扩张到其它的调用方法里去了
*/
return new OtherClass();
}
// 实例引用传递逃逸
public void instancePassPointerEscape(){
/**
* 这里的this 代表EscapeTest类,传递到了OtherClass类里。
* this的作用域原先是在当前类里,现在扩张到了 OtherClass 类里
*/
this.methodPointerEscape().someMethod(this);
}
//线程逃逸
/**
* 就是赋值给类变量或可以在其它线程中访问的实例变量。
*/
class OtherClass{
public void someMethod(EscapeTest escapeTest){
System.out.println(escapeTest.getClass().getName());
}
}
}
Jvm在做逃逸分析的时候,会针对这些场景进行分析,分析完成之后,会为对象做一个逃逸状态标记,一般有三种逃逸状态
- 全局级别逃逸:一个对象可能从方法或者当前线程中逃逸。也就是说其它的方法,或其它的线程也可以访问到这个对象,有以下爱几个场景
- 对象作为方法的返回值
- 对象作为静态字段(static field)或者成员变量(field)
- 如果重写了某个类的 finalize() 方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中。
- 参数级别逃逸:对应实例引用传递逃逸
- 对象被作为参数传递给一个方法,但是在这个方法之外无法访问其它的线程也无法访问到这个对象。
- 无逃逸:一个对象不会逃逸
标量替换
标量:不能被进一步分解的量。比如:
- 基础数据类型
- 对象引用
聚合量:可以进一步分解的量,比如字符串,因为字符串是字节数组实现的,可以被分解。比如我们自己定义的类,里面有属性可以被分解。
标量替换指的是经过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是创建它的成员变量来代替。
-XX:+EliminateAllocations 开启标量替换(Jdk 8 默认开启)
代码举例: 就不需要创建 someClass 对象了。不需要分配内存空间了。
/**
* 标量替换
*/
public class VariableReplace {
public void method(){
SomeClass someClass = new SomeClass();
someClass.age = 4;
someClass.id = 9;
// 开启标量替换后,优化成代码变成如下:
int age = 4;
int id = 9;
//就不需要创建 someClass 对象了。不需要分配内存空间了
}
class SomeClass {
int id;
int age;
}
}
栈上分配
通过逃逸分析,能够确认对象不会被外部访问,就在栈上分配对象。
对象分配在栈帧中,随着方法的结束,栈帧出栈,对象也被清理了。通过栈上分配,可以减少垃圾回收的压力
锁消除是 synchronized 相关的优化