逃逸分析:解锁性能的神秘钥匙!

淩亂°似流年 2024-02-19 08:15 38阅读 0赞

优质博文:IT-BLOG-CN

面试管坑位:在Java中新创建的对象一定是在堆上分配内存吗?如果你的答案是“是的”那就需要看看这个文章了。

一、简介

逃逸分析Escape Analysis:是一个很重要的JIT优化技术,用于判断对象是否会在方法外部被访问到,也就是逃出方法的作用域。逃逸分析是JIT编译器的一个步骤,通过JIT我们能够确定哪些对象可以被限制在方法内部使用,不会逃逸到外部,然后可以对它们进行优化,比如把它们分配在栈上而不是堆上,或者进行标量替换,把一个对象拆散成多个基本类型来存储。是一种可以有效减少Java程序中同步负载内存堆分配和垃圾回收压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新对象的引用的使用范围,而决定是否要将这个对象分配到堆上

逃逸分析主要针对局部变量,判断堆上分配的对象是否逃逸出方法的作用域。它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸Escape。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。合理地设计代码结构和数据的使用方式能够更好地利用逃逸分析来优化程序的性能。我们还可以通过逃逸分析减少堆上分配对象的开销,提高内存利用率。

逃逸分析不是直接的优化手段,而是代码分析手段。

二、逃逸分析的好处

【1】栈上分配,可以降低垃圾收集器运行的频率。
【2】同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
【3】标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有:减少内存使用,因为不用生成对象头。和程序内存回收效率高,并且GC频率也会减少。因此对于临时对象或短期使用的对象,尽量使用局部变量来存储,以减少对象逃逸的可能性。对于复杂的数据结构,尽量使用基本类型、数组或集合类,以减少对象的分配和逃逸。
【4】使用final关键字来限制对象的可变性,这样JIT编译器更容易进行逃逸分析和优化。

三、why

栈上分配Stack Allocations:在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换Scalar Replacement 若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型都不能再进一步分解,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量AggregateJava中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

同步消除Synchronization Elimination 线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。需要注意的是:这种情况针对的是synchronized锁,而对于Lock锁,则JVM并不能消除。

代码说明: 但是在实际的应用程序中,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或分析过程耗时但却无认开启的选项。如果有需要用户可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析法有效判别出非逃逸对象而导致性能下降。

  1. public class EscapeTest {
  2. /**
  3. * JIT编译时会对代码进行逃逸分析
  4. * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
  5. * Person没有逃逸
  6. */
  7. private static String alloc() {
  8. Person person = new Person();
  9. return person.toString();
  10. }
  11. /**
  12. * 同步省略(锁消除)JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
  13. */
  14. public void append(String str1, String str2) {
  15. StringBuffer stringBuffer = new StringBuffer();
  16. stringBuffer.append(str1).append(str2);
  17. }
  18. /**
  19. * 标量替换
  20. */
  21. private static void test2() {
  22. Point point = new Point(1,2);
  23. System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
  24. // 编译后的伪代码,也就是常说的内联后的样子
  25. // int x=1;
  26. // int y=2;
  27. // System.out.println("point.x="+x+"; point.y="+y);
  28. }
  29. }

四、结论

关于逃逸分析的研究论文早在1999年就已经发表,但直到JDK 6HotSpot才开始支持初步的逃逸分析,而且到现在这项优化技术尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是逃逸分析的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗。如果要百分之百准确地判断一个对象是否会逃逸,需要进行一系列复杂的数据流敏感的过程间分析,才能确定程序各个分支执行时对此对象的影响。前面介绍即时编译、提前编译优劣势时提到了过程间分析这种大压力的分析算法正是即时编译的弱项。可以试想一下,如果逃逸分析完毕后发现几乎找不到几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成分析。这个在JIT优化实战中有说明。

jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指 定是否开启逃逸分析:

  1. XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
  2. XX:‐DoEscapeAnalysis //表示关闭逃逸分析。
  3. XX:+EliminateAllocations //开启标量替换(默认打开)
  4. XX:+EliminateLocks //开启锁消除(jdk1.8默认开启)

开启逃逸与关闭逃逸的区别:
【1】关闭逃逸:-XX:-DoEscapeAnalysis -XX:+PrintGC

  1. long start = System.currentTimeMillis();
  2. for(int i=0;i<5000000;i++){
  3. newObject();
  4. }
  5. long end = System.currentTimeMillis();
  6. System.out.println("耗时"+(end-start)+"毫秒");
  7. Thread.sleep(100000);

结果:41毫秒,一次GC并且有一百多万的垃圾回收。

  1. [GC (Allocation Failure) 65536K->880K(251392K), 0.0013300 secs]
  2. 耗时41毫秒
  3. num #instances #bytes class name
  4. 1: 1088834 17868374 java.lang.Object

【2】开启逃逸:-XX:+DoEscapeAnalysis -XX:+PrintGC 只有4毫秒,没有GC,提高了快10倍效率,并且堆中只有十几万。逃逸了

  1. 耗时4毫秒
  2. num #instances #bytes class name
  3. 1: 14534 2734633 java.lang.Object

可以发现一个逃逸和没逃逸的问题,只要是对象有被方法外部或者全局引用到那肯定会存在逃逸。当对象没有发生逃逸的时候,虚拟机会对其进行优化。

发表评论

表情:
评论列表 (有 0 条评论,38人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM逃逸分析

      我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存。如果堆中对象数量太多,回收对象还有整理内

    相关 详解逃逸分析

    Go是一门带有垃圾回收的现代语言,它抛弃了传统C/C++的开发者需要手动管理内存的方式,实现了内存的主动申请和释放的管理。Go的垃圾回收,让堆和栈的概念对程序员保持透明,它增加

    相关 JVM逃逸分析

    摘要: 本文基于周志明著作的《深入了解Java虚拟机》主要介绍了逃逸分析的定义,以及逃逸分析的一些应用,方便复习 `逃逸分析`(Escape Analysis)是目前Jav

    相关 Go 逃逸分析

    1 前言 所谓逃逸分析(Escape analysis)是指由编译器决定内存分配的位置,不需要程序员指定。 函数中申请一个新的对象 如果分配 在栈中,则函数执行结束可自...