您的位置  > 互联网

Java中的对象和数组都是在堆上分配的吗?

自从开始学习Java以来​​,我们就接触到这样的观点:Java中的对象是在堆上创建的,对象的引用是放在栈上的。 这种观点真的正确吗? 如果是正确的,那么为什么面试官会问:“Java中的对象必须分配在堆上吗?” 这个问题呢? 看来我们自从接触Java以来​​所接受的观点是值得我们怀疑的。

关于面试问题

标题中的面试题是:Java中的对象和数组是在堆上分配的吗?

当面试官问到这个问题时,有的朋友会心想:我从一开始学Java就知道:Java中的对象是在堆上创建的,对象的引用是存储在栈上的。 那么Java中的对象和数组一定是分配在堆上的! 是不是?

如果你这样回答,你就直接通过了。

可能有些朋友还不太明白,我们继续往下看。

面试问题及答案

首先我们来回答一下这个问题。 这里我简单回答一下这个面试问题,稍后我们会进行相关分析。

你可以这样回答:Java中的对象不一定分配在堆上,因为通过逃逸分析,JVM可以分析一个新对象的使用范围,从而决定是否将该对象分配到堆上。 如果JVM发现某些对象没有逃逸方法,则很可能会优化为分配在堆栈上。

这里,我们接触到一个新术语:逃逸分析。 相信很多朋友还不是很明白,我们继续往下看。

逃逸分析 逃逸分析的概念

我们先来说说官方形式的逃逸分析是什么。 逃逸分析是:确定指针动态范围的静态分析。 它可以分析程序中哪里可以访问指针。

在JVM即时编译的背景下,逃逸分析将确定新创建的对象是否逃逸。 即时编译判断对象是否逃逸的依据:一是该对象是否存储在堆中(堆中对象的静态字段或实例字段),二是该对象是否被传入未知代码。

直接讲这些概念有点混乱,所以我们举两个例子。

对象转义示例

典型的对象转义是:将对象复制到成员变量或静态变量中,并可能被外部使用。 这时,变量就逃逸了。

我们可以用下面的代码来表示这种现象。

/**
 * @author binghe
 * @description 对象逃逸示例1
 */

public class ObjectEscape{
    private User user;
    public void init(){
        user = new User();
    }
}

在类中,有一个成员变量user。 在init()方法中,我们创建了一个User类的对象,并将其赋值给成员变量user。 此时对象被复制到成员变量中,并可能被外部使用,此时变量就逃逸了。

另一种典型场景是:通过语句返回对象。 如果通过语句返回该对象,此时程序无法确定该对象以后是否会被使用。 外部线程可以访问这个变量,此时对象也逃逸。

我们可以用下面的代码来表示这种现象。

/**
 * @author binghe
 * @description 对象逃逸示例2
 */

public class ObjectReturn{
    public User createUser(){
        User user = new User();
        return user;
    }
}

给出两个例子,相信你对JVM的逃逸分析有了一定的了解。 是的,JVM可以通过逃逸分析来分析新对象的使用范围,从而决定新对象是否应该分配在堆上。

还没结束,我们继续看看逃逸分析的优点,以便小伙伴们更好的理解逃逸分析。

逃逸分析的优点

逃逸分析的优点一般可以分为三点:对象可以在堆栈上分配、分离对象或标量替换、消除同步锁。 我们可以用下图来表示这一点。

对象可以在堆栈上分配

JVM通过逃逸分析来分析新对象的使用范围,然后在堆栈上分配该对象。 栈分配可以在栈帧上快速创建和销毁对象,而不需要将对象分配到堆空间,可以有效减轻JVM垃圾收集的压力。

分离对象或标量替换

当JVM通过逃逸分析决定将一个对象分配到堆栈时,即时编译可以分解该对象并用小的局部变量替换该对象。 我们将这种分解过程称为标量替换。 将对象替换为局部变量后,可以非常方便地在堆栈上进行分配。

同步锁消除

如果JVM通过逃逸分析发现某个对象只能从一个线程访问,则在访问该对象时不需要添加同步锁。 如果程序中使用了锁,JVM就会消除该锁。

这里需要注意的是,这种情况是针对锁而言的,对于Lock锁来说,JVM是无法消除的。

要启用同步消除,需要添加-XX:+参数。 由于该参数依赖于转义分析,因此必须同时打开-XX:+选项。

因此,并非所有对象和数组都分配在堆上。 由于即时编译的存在,如果JVM发现某些对象没有转义方法,那么它们很可能会被优化为分配在堆栈上。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

长按订阅更多精彩▼

如有收获,点个在看,诚挚感谢