Java中的引用逃逸与标量替换

大家一般认为new出来的对象都是被分配在堆上,但这并不是完全正确,通过对Java对象分配过程分析,我们发现对象除了可以被分配在堆上,还可以在栈或TLAB中分配空间。而栈上分配对象的技术基础是逃逸分析和标量替换。

一、什么是引用逃逸

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

如果一个方法中分配一个对象并返回一个该对象的引用针,那么该对象可能被访问到的地方就无法确定,此时对象的引用就发生了“逃逸”。如果对象的引用存储在静态变量或者其它数据结构中,因为静态变量是可以在当前方法之外访问到,此时对象的引用也发生了“逃逸”。

逃逸分析确定某个对象的引用可以被访问的所有地方,以及确定能否保证对象的引用的生命周期只在当前进程或线程中。

  public static StringBuffer runTest(String test1,String test2){
        StringBuffer stringBuffer=new StringBuffer();
        stringBuffer.append(test1);
        stringBuffer.append(test2);
        return stringBuffer;
    }

在上述方法中,runTest中创建出来的StringBuffer对象最后被返回到了方法的外部,这样当我们这个方法结束后,StringBuffer对象就可能无法被垃圾回收,这就发生了逃逸,而在下面这段代码中,我们将StringBuffer对象转换为了一个字符串,这样在我们看来这个StringBuffer对象就没有发生逃逸。

 public static String runTest(String test1,String test2){
        StringBuffer stringBuffer=new StringBuffer();
        stringBuffer.append(test1);
        stringBuffer.append(test2);
        return stringBuffer.toString();
    }

二、逃逸类型

(一)全局逃逸

即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

对象是一个静态变量;对象是一个已经发生逃逸的对象;对象作为当前方法的返回值。

(二)参数逃逸

即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

(三)无逃逸现象

当前方法的对象没有发生逃逸现象。

三、如何实现逃逸分析优化

(一)标量替换

首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。

这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。

标量替换的 JVM 参数如下:

开启标量替换:-XX:+EliminateAllocations

关闭标量替换:-XX:-EliminateAllocations

显示标量替换详情:-XX:+PrintEliminateAllocations

标量替换同样在 JDK8 中都是默认开启的。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换

public static void main(String[] args) {
   alloc();
}

private static void alloc() {
   Point point = new Point(1,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);
}

(二)栈上分配

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。

关于栈上分配的详细介绍,可以参考对象和数组并不是都在堆上分配内存的。

这里,还是要简单说一下,其实在现有的虚拟机中,并没有真正的实现栈上分配,在对象和数组并不是都在堆上分配内存的。我们的例子中,对象没有在堆上分配,其实是标量替换实现的。

(三)同步省略

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

锁消除的 JVM 参数如下:

开启锁消除:-XX:+EliminateLocks

关闭锁消除:-XX:-EliminateLocks

锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。

(四)总结

所以,如果以后再有人问你:是不是所有的对象和数组都会在堆内存分配空间?

那么你可以告诉他:不一定,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。就像我们前面看到的一样,在开启逃逸分析之后,也并不是所有User对象都没有在堆上分配。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值