您的位置:首页 > 编程语言 > Java开发

Java内存对象的逃逸分析

2011-12-02 13:54 357 查看
 

    逃逸分析英文作Escape Analysis。在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。
    当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他过程或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。
    在Java中比如下面的方法:
......
static V global_v;
public void a_method(){
   V v=b_method();
   c_method();
}
public V b_method(){
   V v=new V();
   return v;
}
public void c_method(){
   global_v=new V();
}
    其中b_method方法内部生成的V对象的引用被返回给a_method方法内的变量v,c_method方法内生成的V对象被赋给了全局变量global_v。这两种场景都发生了指针(引用)逃逸。
    逃逸分析研究对于Java编译器有什么好处?我们知道Java对象总是在堆中分配的,因此Java对象的创建和回收对系统的开销是很大的。Java语言被批评的一个地方,也是认为Java性能慢的一个原因就是Java不支持运行时栈分配对象,缺少像C#里面的值对象或者C++里面的struct结构。
    前面一篇文章《Java并发编程-常量对象(七)》就曾讨论过这个问题,这是Swing内存和性能消耗的瓶颈。近几年业界曾进行过激烈讨论,Java 6中是否应该加入栈分配对象。其中有人主张可以通过JIT进行逃逸分析的方式来解决目前的问题,不用在语言级别进行支持,虚拟机支持栈分配机制,由JIT对代码进行内联优化和逃逸分析。
    那么JIT怎么通过逃逸分析进行代码优化呢?分析下面的过程代码:
public void my_method(){
    V v=new V();
    //use v
    ......
    v=null;
}
    在这个方法中创建的局部对象被赋给了v,但是没有返回,没有赋给全局变量等等操作,因此这个对象是没有逃逸的,是可以在运行时栈进行分配和销毁的对象。没有发生逃逸的对象由于生命周期都在一个方法体内,因此它们是可以在运行时栈上分配并销毁。
    这样在JIT编译Java伪代码时,如果能分析出这种代码,那么非逃逸对象其创建和回收就可以在栈上进行,从而能大大提高Java的运行性能。
    另外为什么要在逃逸分析之前进行内联分析呢?这是因为往往有些对象在被调用过程中创建并返回给调用过程,调用过程使用完该对象就销毁了。这种情况下如果将这些方法进行内联,它们就由两个方法体变成一个方法体了,这种原来通过返回传递的对象就变成了方法内的局部对象,就变成了非逃逸对象了,这样这些对象就可以在同一栈上进行分配了。
    据说Java 6的虚拟机已经支持对象的栈分配和逃逸分析机制了,但目前并没有启动。具可靠的消息,Java 7中将启动这一功能。这对Swing来说又是一次大规模提升速度的机会。
    除能将堆分配对象变成栈分配对象,逃逸分析还有其他两个优化应用。一是同步消除。我们知道线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。
    二是矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。
    Java 7将完全支持了栈式分配对象,JIT将支持逃逸分析优化,另外Java 7还将缺省支持OpenGL的加速功能,光这三种平台性能的提升就会给Swing带来又一次性能的革命。
    Swing的未来越来越光明


 
 

逃逸分析(Escape Analysis)  

2011-05-12 23:31:21|  分类:

IT |  标签:
|字号大中小 订阅

       在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析。

       通俗一点讲,就是当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。

       而用来分析这种逃逸现象的方法,就称之为逃逸分析。

       举个例子:
Java代码
class A {

    public static B b;  

    public void globalVariablePointerEscape() { // 给全局变量赋值,发生逃逸

    b = new B();

    }  

    public B methodPointerEscape() { // 方法返回值,发生逃逸

    return new B();

    }  

     public void instancePassPointerEscape() {

     methodPointerEscape().printClassName(this); // 实例引用传递,发生逃逸

    }

}  

 class B {

    public void printClassName(A a) {

        System.out.println(a.class.getName());

    }

}
在这个例子中,一共举了3种常见的指针逃逸场景。分别是 全局变量赋值,方法返回值,实例引用传递。

逃逸分析优化JVM原理

我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针。

当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量较多,将给GC带来较大压力,也间接影响了应用的性能。减少临时对象在堆内分配的数量,无疑是最有效的优化方法。
接下来,举一个场景来阐述。

假设在方法体内,声明了一个局部变量,且该变量在方法执行生命周期内未发生逃逸(在方法体内,未将引用暴露给外面)。

按照JVM内存分配机制,首先会在堆里创建变量类的实例,然后将返回的对象指针压入调用栈,继续执行。

这是优化前,JVM的处理方式。
逃逸分析优化 – 栈上分配

优化原理:分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量也被回收。

这是优化后的处理方式,对比可以看出,主要区别在栈空间直接作为临时对象的存储介质。从而减少了临时对象在堆内的分配数量。
逃逸分析另一个重要的优化 – 锁省略

如果通过逃逸分析能够判断出指向某个局部变量的多个引用被限制在同一方法体内,并且所有这些引用都不能“逃逸”到这个方法体以外的地方,那么HotSpot会要求JIT执行一项优化动作 – 将局部变量上拥有的锁省略掉。

这就是锁省略(lock elision)。
性能测试

class DoubleSlot {      final int value1;      final int value2;         public DoubleSlot(int value1, int value2) {        this.value1 = value1;        this.value2 = value2;      }    }       static int slotValue(DoubleSlot slot) {      return slot.value1 + slot.value2;    }       static int sum(int[] values) {      int sum = 0;      int length = values.length;      for(int i=1; i<100; i++)        sum(values);         for(int i=0; i<100; i++)        test(values);    }

测试结果是:

$ /usr/jdk/jdk1.6.0_14/bin/java -server EscapeAnalysisTest

time 8889261

$ /usr/jdk/jdk1.6.0_14/bin/java -server -XX:+DoEscapeAnalysis EscapeAnalysisTest

time 1408140
从结果中,可以看到,启用逃逸分析的运行性能6倍于未启用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息