JAVA逃逸分析

总结:
在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
我们知道Java对象是在堆里分配的,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力。因此,减少临时对象在堆内存分配的数量是最有效的优化方法。

逃逸分析包括:

  • 全局变量赋值逃逸
  • 方法返回值逃逸
  • 实例引用发生逃逸
  • 线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量.
public class EscapeAnalysis {

     public static Object object;

     public void globalVariableEscape(){//全局变量赋值逃逸  
         object =new Object();  
      }  

     public Object methodEscape(){  //方法返回值逃逸
         return new Object();
     }

     public void instancePassEscape(){ //实例引用发生逃逸
        this.speak(this);
     }

     public void speak(EscapeAnalysis escapeAnalysis){
         System.out.println("Escape Hello");
     }
}

使用方法逃逸的案例进行分析:

 public StringBuffer createString(String ... values){
         StringBuffer stringBuffer = new StringBuffer(); 
         for (String string : values) {
             stringBuffer.append(string+",");
        }
         return stringBuffer;
     }

     public static void main(String[] args) {
        StringBuffer sb = new EscapeAnalysis().createString("Escape","Hello");
        System.out.println(sb.toString());
    }

从上面的案例我们看出stringBuffer是属于方法返回值逃逸。我们可以通过改变返回值得类型为String限定了StringBuffer的作用域在createString方法中从而不发生逃逸。

 public String createString(String ... values){
         StringBuffer stringBuffer = new StringBuffer(); 
         for (String string : values) {
             stringBuffer.append(string+",");
        }
         return stringBuffer.toString();
     }

     public static void main(String[] args) {
         String string = new EscapeAnalysis().createString("Escape","Hello");
        System.out.println(string);
    }

标量替换

1.标量和聚合量
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。

2.替换过程
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。

通过-XX:+EliminateAllocations可以开启标量替换, -XX:+PrintEliminateAllocations查看标量替换情况(Server VM 非Product版本支持)

栈上分配

其实,在java应用里普遍存在一种场景。一般是在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。这是优化前,JVM的处理方式。
• 逃逸分析优化 - 栈上分配
分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。对比可以看出,主要区别在栈空间直接作为临时对象的存储介质。从而减少了临时对象在堆内的分配数量。
故名思议就是在栈上分配对象,其实目前Hotspot并没有实现真正意义上的栈上分配,实际上是标量替换。

通过-XX:-DoEscapeAnalysis关闭逃逸分析
测试逃逸分析后堆内存对比:

private  int count = 1000000;

    public static void main(String[] args) throws InterruptedException, IOException {
        EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
        for (int i = 0; i < escapeAnalysis.count ; i++) {
            escapeAnalysis.getAge();
        }
        Thread.sleep(500);
        for (int i = 0; i < escapeAnalysis.count ; i++) {
            escapeAnalysis.getAge();
        }
        System.in.read();
    }


    public int  getAge(){
        Person person = new Person("小明",18,28.1);   
        return person.getAge();
    }

    class Person {

        private String name;

        private int age;

        private double weight;

        public Person(String name, int age, double weight) {
            super();
            this.name = name;
            this.age = age;
            this.weight = weight;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public double getWeight() {
            return weight;
        }

        public void setWeight(double weight) {
            this.weight = weight;
        }

    }

通过jmap -histo [pid]查看java堆上的对象分布情况:

  • 关闭逃逸分析的数据

    C:\Users\li>jmap -histo 16592
    
    num     #instances         #bytes  class name
    ----------------------------------------------
     1:        980000       31360000  test.EscapeAnalysis$Person
     2:           154         785000  [I
     3:          2158         288504  [C
     4:           489          55832  java.lang.Class
     5:          2017          48408  java.lang.String
     6:           839          33560  java.util.TreeMap$Entry
  • 启用逃逸分析的数据

    C:\Users\li>jmap -histo 7100
    
    num     #instances         #bytes  class name
    ----------------------------------------------
     1:        229881        7356192  test.EscapeAnalysis$Person
     2:           446         756944  [I
     3:          3105         442024  [C
     4:          2408          57792  java.lang.String

    通过上面数据可以看出没有开启逃逸分析时,Person在堆的内存是31360000 ,而开启逃逸分析时,Person在堆中的内存为7356192。 两者之间相差24003808。证明了启用了逃逸分析,可以减少堆内存的使用和减少GC。

同步消除

在即使编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。

通过-XX:+EliminateLocks可以开启同步消除,进行测试执行的效率

 public String createString(String ... values){
         StringBuffer stringBuffer = new StringBuffer(); 
         for (String string : values) {
             stringBuffer.append(string+" ");
        }
         return stringBuffer.toString();
     }   
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
        for (int i = 0; i < 1000000; i++) {
            escapeAnalysis.createString("Escape", "Hello");
        }
        long bufferCost = System.currentTimeMillis() - start;
        System.out.println("craeteString: " + bufferCost + " ms");
    }


优化前:
it takes 867 ms
优化后:
it takes 802 ms

基于逃逸分析,JVM可以判断,如果这个局部变量StringBuffer并没有逃出它的作用域,那么可以确定这个StringBuffer并不会被多线程所访问,那么就可以把这些多余的锁给去掉来提高性能。

Reference:

http://rednaxelafx.iteye.com/blog/659108/