按学院派的风格来说,其实栈上分配是指:对于那些线程私有的对象,可以将它们打散分配在栈上,而不是分配在堆上。它是 Java 虚拟机提供的一项优化技术,可以提高虚拟机的运行性能。

看了上面的定义,是不是感觉有点晕。没关系,下面我们一项一项来理清思绪。

堆与栈

稍微了解过 Java 虚拟机内存结构的同学都知道,在 Java 虚拟机中有两个关键的存储数据节点,那就是:堆与栈。

其中堆是所有线程共享的一块内存,几乎所有对象的分配都在这块内存中。而栈则是线程自己私有的,只存储线程自己的局部变量等信息。每个线程都有自己的栈,栈信息无法在线程之间共享。

1.jpg

一般情况下,每个线程如果有新建的对象,那么会跟 JVM 申请在堆上创建对应的对象,而线程的栈则存储了指向堆对象的指针。每当一个线程想创建一个对象时,首先会请求 JVM,之后 JVM 进行协调,创建完成之后再告诉线程,线程最后将引用放到栈中。

在对象创建的这个过程,堆和栈之间的关系就像是列车的中央调度室和火车的关系。每次火车要发车,都得等待中央调度室协调发号施令,会耗费不少时间和精力。

这个时候有人就发现,线程的有些对象其实别人也不会访问到,放在堆中貌似也没什么大作用。于是他提出:对于这些其他线程不会访问的对象,我们能不能让线程自己分配在它自己的栈空间上?这样不就可以节省不少交互时间了么!

这个方法确实不错,如果能实现应该可以提高对象创建的时间,提高虚拟机的运行效率。但问题是:我怎么知道哪些对象可以分配在栈上,哪些不行呢?

逃逸分析

其实聪明的软件工程师们早就解决了这个问题了,他们新造了一个名字:逃逸分析。专门来分析一个对象是否可以分配在栈上。

那么什么是逃逸分析呢?

从字面意思上来讲,逃逸分析的目的是判断对象的作用域是否有可能逃出函数体。例如下面的代码就显示了一个逃逸的对象:

private static User user;
private static void hello(){
   u = new User();
   u.name = "java.top.select";
   u.website = "http://www.shuyi.me";
}

对象实例 user 是类的成员变量,可以被任何线程访问,因此它属于逃逸对象。但如果我们将代码稍微改动一下,该对象就可以线程非逃逸的了。

private static void hello(){
   User u = new User();
   u.name = "java.top.select";
   u.website = "http://www.shuyi.me";
}

可以看到 user 实例作用域只在 hello 函数中,不会被其他线程访问到,也不会访问。所以该 user 实例对象的作用域只在该函数中,因此它并未发生逃逸。对于这样的情况,虚拟机就有可能将其分配在栈上,而不在堆上。

看到这里,我相信许多人都应该明白了什么是栈上分配了。简单点说,就是将本来应该分配在堆中的对象,让其分配在线程私有的栈上。通过这种方式,减少垃圾回收的压力,提高虚拟机的运行效率。

动手验证

上面说了这么多,但没有做过实际的实验,怎么知道说得对不对啊。下面就让我们来做个简单的测试,看看 JVM 是否真的有栈上分析这回事!

public class OnStackDemo {
    public static void alloc(){
        User u = new User();
        u.name = "java.top.select";
        u.website = "http://www.shuyi.me";
    }

    public static void main(String[] args) 
        for(int i = 0; i < 100000000; i++) {
            alloc();
        } 
    }
}

上面的代码中,我们新建了一亿个 User 对象。如果他们没有启用栈上分配的话,那么它们必定会产生 GC。如果没有产生GC,那么就可以判断它们使用了栈上分配的优化技术。

下面使用 javac 命令编译后,使用如下面命令运行该程序:

java -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations com.chenshuyi.OnStackDemo

其中 -server 表示使用 Server 模式运行 JVM,因为只有在 Server 模式下,才可以启用逃逸分析。另外我们指定了非常小的堆大小,使得其堆空间相对于一千万的对象大小远远不足。运行后,程序直接并没有产生GC。这就说明了其使用了栈上分配的技术。

写在最后

对于大量的零散小对象,使用栈上分配技术可以很好地提高对象分配效率,可以避免小对象对垃圾回收带来的负面影响。但另一方面,与堆空间相比,栈空间毕竟很小,所以打对象不适用于栈上分配。

看到这里我相信大部分人都能明白栈上分配的价值所在了,虚拟机也正是因为有着许多类似栈上分配技术的优化,才现在如此强大。