对象一定在堆上分配吗?论逃逸分析、栈上分配、标量替换和锁消除

264 阅读3分钟

在传统对象分配当中,通常在堆上进行对象实例的分配,但是随着JIT编译器的发展和逃逸分析技术的成熟,在栈上分配内存也不那么绝对了。下面简单介绍逃逸分析、栈上分配、标量替换和锁消除


逃逸分析

hotSpot虚拟机可以利用相应算法,判断对象是否逃逸,从而实现对象是否在栈上分配还是堆分配的一项技术。

逃逸又分为几种情况:

1、全局逃逸(GlobalEscape) 即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:

对象是一个静态变量

    static Object global_v;
    public static void main(String[] args){
        new Test().c_method();
    }
    public void c_method(){
        global_v=new Object();
    }

对象作为当前方法的返回值

    public static void main(String[] args){
        new Test().a_method();
    }
    public StringBuilder a_method(){
        StringBuilder builder =new StringBuilder();
        return builder;
    }

对象是一个已经发生逃逸的对象

    public static void main(String[] args){
        new Test().b_method();
    }
    public StringBuilder a_method(){
        StringBuilder builder =new StringBuilder();
        return builder;
    }
    public String b_method(){
        StringBuilder builder = a_method();
        return builder.toString();
    }

2、参数逃逸(ArgEscape)

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

    public static void main(String[] args){
        new Test().a_method();
    }
    public String a_method(){
        StringBuilder builder =new StringBuilder();
        String s = d_method(builder);
        return s;
    }
    public String d_method(StringBuilder builder){
        return builder.toString();
    }

3、没有逃逸 即方法中的对象没有发生逃逸。

    public static void main(String[] args){
        new Test().d_method();
    }

    public String d_method(){
        StringBuilder builder =new StringBuilder();
        return builder.toString();
    }

简单的来说,就是对象的生命周期只在局部方法内,没有全局的变量引用或者脱离掉自己作用域的引用。这种对象就没有发生逃逸,可以进一步的优化

逃逸分析的 JVM 参数如下:

开启逃逸分析:-XX:+DoEscapeAnalysis

关闭逃逸分析:-XX:-DoEscapeAnalysis


1.栈上分配

jvm参数配置如下

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC

    public static void alloc() {
        byte[] b = new byte[2];
        b[0] = 1;
    }

    public static void main(String[] args) {
        long b = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }

关闭逃逸分析 -server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC

不仅gc次数大大增加,速度也相差上百倍之多。实现栈上分配的前提是对象必须未发生逃逸,意味着未逃逸的对象都会直接在栈上分配,避免了对象在堆区的生成,产生gc的压力。在java8中以默认开启逃逸分析。意味着我们在写程序时,没有必要把大对象的引用脱离方法作用域。从而实现栈上分配,及时回收。

2.标量替换

标量是指一个不可分割的量,比如基本类型,与标量对应的是聚合量,常见的就是对象,在对象内部还是其他对象属性。在逃逸分析的基础上,聚合量的属性可以成为一个个标量直接在栈上分配,不用在堆分配,用完即丢。标量替换同样在 JDK8 中都是默认开启的

标量替换的 JVM 参数如下:

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

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

3.锁消除

我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。比如Stringbuffer

public static void alloc() {
    StringBuffer buffer = new StringBuffer();
    System.out.println("hash: " + buffer.hashCode());
    System.out.println(ClassLayout.parseInstance(buffer).toPrintable());
}

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    long b = System.currentTimeMillis();
    alloc();
    long e = System.currentTimeMillis();
    System.out.println(e - b);
}

按照打印的对象头从高到低打印,表示锁信息的记录为00000001后三位001,对应的表中的锁状态为无锁状态。表明StringBuffer在局部方法内无锁。同步消除同样需要逃逸分析,建立在对象未逃逸的情况下。java8中以默认开启。

锁消除的 JVM 参数如下:

开启锁消除:-XX:+EliminateLocks

关闭锁消除:-XX:-EliminateLocks

参考文章:

zhuanlan.zhihu.com/p/69136675

blog.csdn.net/blueheart20…