逃逸分析概念
随着jtl编译器的发展,与逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术将会导致对象分配到堆上的这个结论变得不是那么地绝对了,如果经过逃逸分析后,一个对象并没有逃逸出方法,那么就有可能被优化成站上分配,而判断一个对象是否逃逸则是通过判断该对象是否有可能在方法外使用。
逃逸分析是一种 有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
逃逸分析开关
默认是开启的,但可以通过参数-XX:+DoEscapeAnalysis进行开启关闭
逃逸分析场景分析
package com.lydon.test;
public class EscapeAnalysis {
private EscapeAnalysis escapeAnalysis;
/**
* 方法直接返回对象,发生逃逸现象的对象escapeAnalysis
* @return
*/
public EscapeAnalysis getInstance(){
if(escapeAnalysis==null){
escapeAnalysis=new EscapeAnalysis();
}
return escapeAnalysis;
}
/**
* 将对象直接暴露在方法外,有可能被其他方法使用,发生了逃逸现象
* @param
*/
public void setObj(){
this.escapeAnalysis=new EscapeAnalysis();
}
/**
* 对象在内部使用,所以没有发生逃逸现象
*/
public void useEscapAnalysis(){
EscapeAnalysis escapeAnalysis=new EscapeAnalysis();
}
/**
* getInstance返回了对象,此方法也发生了逃逸现象
*/
public void useEscapAnalysis2(){
EscapeAnalysis escapeAnalysis=getInstance();
}
/**
* 逃逸分析后逃逸的对象
* @return
*/
public static StringBuilder createStringBuilder(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("1");
stringBuilder.append("2");
return stringBuilder;
}
/**
* 逃逸分析后未逃逸的对象,把stringbuilder放在方法内部了
* @return
*/
public static String createStringBuilderNoEscapeAnalysis(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("1");
stringBuilder.append("2");
return stringBuilder.toString();
}
}
结论就是能在方法内定义的变量就在方法内定义,不要定义成成员变量。
逃逸分析-代码优化
在逃逸分析的基础上,jvm可以自动进行一些优化措施,分别为栈上分配、同步省略、标量替换。要注意的是这些优化措施是在执行器编译时优化的,不是生成字节码时优化的。
1、栈上分配
通过逃逸分析后,发现未出现逃逸现象,因此该对象可以是栈分配的候选,可以优化成栈分配,而不是堆分配。在在调用栈结束后,栈被弹出,局部变量也被回收,这样就不必做垃圾回收了。不能进行栈上分配的场景有给成员变量赋值,返回对象实例,实例引用传递(也就是上面的代码段中经过逃逸分析后是属于逃逸现象的场景)。
下面写个代码试一下开启和关闭逃逸分析的区别:
package com.lydon.test;
public class StackAllocation {
public static void main(String[] args) {
long start=System.currentTimeMillis();
for (int i=0;i<1000000000;i++){
alloc();
}
long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start));
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void alloc(){
User user=new User();
}
}
/**
*
*/
class User{
private int age;
private String name;
}
关闭-Xms1g -Xmx1g -XX:-DoEscapeAnalysis 耗时:5310
发现堆中分配了很多对象
开启-Xms1g -Xmx1g -XX:+DoEscapeAnalysis 耗时:15
没有发现在堆中有分配的现象
2、同步省略
如果发现一个对象只能被一个线程访问到,那么对这个对象的操作可以不加锁,也叫锁消除
public void lock(){
Object o1=new Object();
//因为每个线程都会产生一个o1锁,所以这里加锁是没有必要,o1不是公共锁,所以jvm经过逃逸分析后去掉这个锁
synchronized (o1){
System.out.println("111");
}
}
public void lock2(){
Object o1=new Object();
System.out.println("111");
}
3、分离对象或标量替换
有的对象不需要连续的内存结构来存储也可以被访问到,那么可拆散成局部变量保存在pc寄存器中。
标量:指的是一个无法再被拆解的数据
聚合量:可以被分解的数据,java对象就是聚合量
在JIT阶段,经过逃逸分析后,将不会逃逸的对象拆成各个标量(局部变量)代替,这样就可以达到栈上分配的目的,下面是替换的示例:
/**
* 未替换前的标量分析实例
*/
public static void alloc2(){
Point point = new Point(1,2);
System.out.println(point.x+point.y);
}
/**
* 经过逃逸分析后被替换的标量分析实例
*/
public static void allocAfter(){
int x=1;
int y=1;
System.out.println(x+y);
}
而标量替换是默认开启的,也可通过参数-XX:+EliminateAllocations进行开启或关闭
未开启:-Xms1g -Xmx1g -XX:+DoEscapeAnalysis -XX:-EliminateAllocations
耗时:6094
开启:-Xms1g -Xmx1g -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
耗时:22
这里可以看出一旦标量替换关闭,逃逸分析开启也没用。
逃逸分析小结
在server模式下,才可以开启逃逸分析,-server
虽然可以基于逃逸分析来做栈上分配,锁消除,标量替换,但现在的逃逸分析还不是很成熟,因为逃逸分析额本身也是需要时间的,很难说逃逸分析花费的时间会比分析后做优化的时间短,oracle的jvm并没有实现栈上分配,现在的逃逸分析是建立在标量替换的实现上的,但一旦进行了标量替换,其实也就是变成局部变量了,所以对象实例都是分配在堆上的这个结论仍然是对的。