JVM-逃逸分析浅析

逃逸分析简介

逃逸分析定义:一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。

逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配标量替换同步消除(也叫同步省略)等提供依据,发生逃逸行为的情况有两种:方法逃逸线程逃逸

  • 方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中;
    方法逃逸:
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

使用下面代码测试启动和关闭逃逸分析。

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

运行输出:
JVM-逃逸分析浅析_第1张图片

程序并没有出现GC的情况,说明逃逸分析生效,User字段数据在栈上分配。并且程序运行只需要10ms。

关闭逃逸分析

在启动参数中增加参数:

-XX:+PrintGC -XX:-DoEscapeAnalysis

JVM-逃逸分析浅析_第2张图片
JVM-逃逸分析浅析_第3张图片

程序并出现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 Point1,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);
}

这种替换可以大大减少堆内存的占用,因为一旦不需要创建对象了,那么就不需要分配堆内存了。

栈上分配

故名思议就是在栈上分配对象,其实目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换。

参考:https://blog.csdn.net/qq_31960623/article/details/120178489

你可能感兴趣的:(Java,JVM,jvm,java,逃逸分析)