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

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


逃逸分析

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

逃逸又分为几种情况:

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

对象是一个静态变量

1
2
3
4
5
6
7
static Object global_v;
public static void main(String[] args){
new Test().c_method();
}
public void c_method(){
global_v=new Object();
}

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

1
2
3
4
5
6
7
public static void main(String[] args){
new Test().a_method();
}
public StringBuilder a_method(){
StringBuilder builder =new StringBuilder();
return builder;
}

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

1
2
3
4
5
6
7
8
9
10
11
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)

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

1
2
3
4
5
6
7
8
9
10
11
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、没有逃逸
即方法中的对象没有发生逃逸。

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
    buffer.append("shuaix");
    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

参考文章:

https://zhuanlan.zhihu.com/p/69136675

https://blog.csdn.net/blueheart20/article/details/52050545