逃逸分析简介
逃逸分析定义:一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。
逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除(也叫同步省略)等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸。
- 方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中;
方法逃逸:
public static User createUser(){
User user = new User();
user.setId(713);
user.setName("zl");
user.setAge(18);
return user;
}
非方法逃逸:
public static void createUser(){
User user = new User();
user.setId(713);
user.setName("zl");
user.setAge(18);
}
public static String createUser(){
User user = new User();
user.setId(713);
user.setName("zl");
user.setAge(18);
//User要实现get,set方法,还要实现toString方法
return user.toString();
}
- 线程逃逸:如类变量或实例变量,可能被其它线程访问到;
启用/关闭逃逸分析
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析
从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis
。
-XX:+PrintEscapeAnalysis
来查看分析结果。
使用下面代码测试启动和关闭逃逸分析。
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
EscapeAnalysisTest test = new EscapeAnalysisTest();
test.createUser();
System.out.println("代码执行完毕!耗时:"+(System.currentTimeMillis()-startTime)+"ms");
}
public void createUser(){
int i=0;
while (true){
User user = new User();
user.setId(i++);
user.setName("zl");
user.setAge(20);
if (i>100000000) {
break;
}
}
}
启动逃逸分析
在启动参数中增加参数:
-XX:+PrintGC -XX:+DoEscapeAnalysis
运行输出:
程序并没有出现GC的情况,说明逃逸分析生效,User字段数据在栈上分配。并且程序运行只需要10ms。
关闭逃逸分析
在启动参数中增加参数:
-XX:+PrintGC -XX:-DoEscapeAnalysis
程序并出现GC的情况,说明 对象在堆上分配,并且出现GC回收User对象。并且耗时需要520ms,即便关闭gc打印,也需要470ms,远比开启逃逸分析要慢。
JVM性能优化手段
如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配。
同步消除
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有发布到其他线程。
如果同步块所使用的锁对象通过这种分析被证明只能被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消这部分代码的同步,这个取消同步就叫做同步省略,也叫锁消除。
可以通过-XX:+EliminateLocks
可以开启同步消除。
标量替换
标量即不可被进一步分解的量,而Java的基本数据类型就是标量(比如int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在Java中对象就是可以被进一步分解的聚合量。
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
通过-XX:+EliminateAllocations
可以开启标量替换(JDK7之后默认开启)。
通过-XX:+PrintEliminateAllocations
查看标量替换情况。
在JIT阶段,如果经过逃逸分析,发现一个对象不被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含若干个成员变量来替代。
public static void main(String[] args) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
private int x;
private int y;
}
以上代码中,point对象并没有逃逸出alloc方法,并且point对象是可以拆解成标量的。那么,JIT就会不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。
替换后:
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
}
这种替换可以大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不需要分配堆内存了。
栈上分配
Java虚拟机中,在Java堆上分配创建对象的内存空间几乎是Java程序员都清楚的常识了,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。
虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可回收对象,还是回收和整理内存都需要耗费时间。
如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。
故名思议就是在栈上分配对象,其实目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换。
参考:https://blog.youkuaiyun.com/qq_31960623/article/details/120178489