在传统对象分配当中,通常在堆上进行对象实例的分配,但是随着JIT编译器的发展和逃逸分析技术的成熟,在栈上分配内存也不那么绝对了。下面简单介绍逃逸分析、栈上分配、标量替换和锁消除
逃逸分析
hotSpot虚拟机可以利用相应算法,判断对象是否逃逸,从而实现对象是否在栈上分配还是堆分配的一项技术。
逃逸又分为几种情况:
1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
对象是一个静态变量
1 | static Object global_v; |
对象作为当前方法的返回值
1 | public static void main(String[] args){ |
对象是一个已经发生逃逸的对象
1 | public static void main(String[] args){ |
2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
1 | public static void main(String[] args){ |
3、没有逃逸
即方法中的对象没有发生逃逸。
1 | public static void main(String[] args){ |
简单的来说,就是对象的生命周期只在局部方法内,没有全局的变量引用或者脱离掉自己作用域的引用。这种对象就没有发生逃逸,可以进一步的优化
逃逸分析的 JVM 参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
1.栈上分配
jvm参数配置如下
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
1 | public static void alloc() { |
关闭逃逸分析
-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
参考文章: