JVM之逃逸分析,栈上分配,标量替换——深度解析!

目录

 

 1.对象分配流程总览

1.1逃逸分析所处的阶段

1.2什么是逃逸分析

2.基于逃逸分析的优化

2.1栈分配:

2.2 同步锁消除

2.3分离对象或标量替换。


 1.对象分配流程总览

1.1逃逸分析所处的阶段

    通过上图的对象分配流程,我们可以知道逃逸分析是发生在第一步判断对象是否可以在栈上分配的时候, 在栈上分配的目的是为了减少将对象分配到堆上的概率,节约堆内存,减少GC压力。

    逃逸分析是JVM为了优化对象分配而做的一种优化措施。

1.2什么是逃逸分析

    JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,减轻GC的压力。

    对象逃逸分析就是分析对象动态作用域 。 当一个对象在方法中被定义后,它可能被外部方法所引用。

举个例子:

public User doSomething1() {
   User user1 = new User ();
   user1 .setId(1);
   user1 .setDesc("artisan1");
   // ......
   return user1 ;
}

public void doSomething2() {
   User user2 = new User ();
   user2 .setId(2);
   user2 .setDesc("artisan2");
   // ...... 
}

    doSomething1返回对象,在方法结束之后被返回了,那这个对象的作用域范围是不确定的。

    doSomething2方法可以非常确定的是当方法结束以后,artisan2这个对象就失效了,因为它的作用域范围是当前方法。 那对于这样的对象,JVM会把这样的对象分配到线程栈中,让它随着方法结束时跟随线程栈内存一起被回收掉。
 

2.基于逃逸分析的优化

  当判断出对象不发生逃逸时,编译器可以使用逃逸分析的结果作一些代码优化

  • 堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。
  • 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
  • 分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

             标量替换含义——

              标量: 不可被进一步分解的量,而JAVA的基本数据类型就是标量(比如int,long等基本数据类型) 。

              聚合量: 标量的对立就是可以被进一步分解的量,称之为聚合量。 在JAVA中对象就是可以被进一步分解的聚合量。

              标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。

2.1栈分配:

     对于优化一将堆分配转化为栈分配,这个优化也很好理解。下面以代码例子说明:

     虚拟机配置参数:-XX:+PrintGC -Xms5M -Xmn5M -XX:+DoEscapeAnalysis

     -XX:+DoEscapeAnalysis表示开启逃逸分析,JDK8是默认开启的

     -XX:+PrintGC 表示打印GC信息

     -Xms5M -Xmn5M 设置JVM内存大小是5M

    public static void main(String[] args){
        for(int i = 0; i < 5_000_000; i++){
            createObject();
        }
    }

    public static void createObject(){
        new Object();
    }

    运行结果是没有GC。

    把虚拟机参数改成 -XX:+PrintGC -Xms5M -Xmn5M -XX:-DoEscapeAnalysis。关闭逃逸分析得到结果的部分截图是,说明了进行了GC,并且次数还不少。

[GC (Allocation Failure)  4096K->504K(5632K), 0.0012864 secs]
[GC (Allocation Failure)  4600K->456K(5632K), 0.0008329 secs]
[GC (Allocation Failure)  4552K->424K(5632K), 0.0006392 secs]
[GC (Allocation Failure)  4520K->440K(5632K), 0.0007061 secs]
[GC (Allocation Failure)  4536K->456K(5632K), 0.0009787 secs]
[GC (Allocation Failure)  4552K->440K(5632K), 0.0007206 secs]
[GC (Allocation Failure)  4536K->520K(5632K), 0.0009295 secs]
[GC (Allocation Failure)  4616K->512K(4608K), 0.0005874 secs]

     这说明了JVM在逃逸分析之后,将对象分配在了方法createObject()方法栈上。方法栈上的对象在方法执行完之后,栈桢弹出,对象就会自动回收。这样的话就不需要等内存满时再触发内存回收。这样的好处是程序内存回收效率高,并且GC频率也会减少,程序的性能就提高了。

2.2 同步锁消除

    如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步

    虚拟机配置参数:-XX:+PrintGC -Xms500M -Xmn500M -XX:+DoEscapeAnalysis。配置500M是保证不触发GC。

public static void main(String[] args){
        long start = System.currentTimeMillis();
        for(int i = 0; i < 5_000_000; i++){
            createObject();
        }
        System.out.println("cost = " + (System.currentTimeMillis() - start) + "ms");
    }

    public static void createObject(){
        synchronized (new Object()){

        }
    }

    运行结果

cost = 6ms

    把逃逸分析关掉:-XX:+PrintGC -Xms500M -Xmn500M -XX:-DoEscapeAnalysis

    运行结果

cost = 270ms

    说明了逃逸分析把锁消除了,并在性能上得到了很大的提升。这里说明一下Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别。

2.3分离对象或标量替换。

    这个简单来说就是把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。 二、程序内存回收效率高,并且GC频率也会减少,总的来说和上面优点一的效果差不多

OK,现在我们又知道了一件聪明的JVM在背后为我们做的事了。

 

猜你喜欢

转载自blog.csdn.net/zhangkaixuan456/article/details/107206749