编辑
我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存。如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗,GC表示压力很大,然后影响性能。所以,在我们日常开发中,内存,时间都是相当的宝贵,该如何优化堆栈开销,是一个比较重要的问题。
逃逸分析(Escape Analysis)和栈上分配是对JVM进行优化的常规手段,本文主要深入了解一下。
一、概念
1.逃逸
当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针(或对象)的逃逸(Escape)。
具体而言,分为方法逃逸和线程逃逸两种。
方法逃逸:在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。或者,可以理解成对象跳出了方法。
线程逃逸:这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。对象逃出了当前线程。
2.栈上分配
主要是指在Java程序的执行过程中,在方法体中声明的变量以及创建的对象,将直接从该线程所使用的栈中分配空间。 就是把没发生逃逸的对象,在栈上分配空间。一般而言,创建对象都是从堆中来分配的,这些对象是有可能发生逃逸的。
3.逃逸分析
是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
在JDK 6之后支持对象的栈上分析和逃逸分析,在JDK 7中完全支持栈上分配对象。 其是否打开逃逸分析依赖于以下JVM的设置:
#强制开启: -server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m #关闭逃逸分析: -server -XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
二、为什么需要逃逸分析和栈上分配?
为了GC性能。GC主要回收的对象是堆和方法区。GC不会对栈、程序计数器这些进行回收的,因为没东西可以回收。
如果方法逃逸,那么对象就会分配在堆中,这个时候,GC就要工作了。如果没发生方法逃逸,那么对象就分配在栈中,当方法结束后,资源就自动释放了,GC压根不用操心。所以方法逃逸就是为GC来服务的。GC不运行的时候,程序的性能肯定会好点,不会占用程序运行的时间。虽然GC清扫垃圾的速度很快,但是当一个程序足够大的时候,对象就自然多了,垃圾也自然多了,这个时候GC就忙了。
而进行逃逸分析,就是把那些不会发生逃逸的对象直接分配在栈中,这样不需要GC参与资源的释放,性能自然就会有提升了。
三、 逃逸分析和栈上分配的优劣势分析
1.优势
同步消除:线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。
标量替换:逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。
2.劣势
栈上分配受限于栈的空间大小,一般自我迭代类的需求以及大的对象空间需求操作,将导致栈的内存溢出;故只适用于一定范围之内的内存范围请求。
四、参数配置一览
逃逸分析在JDK6U23以上都是默认开启的,开启后会极大地提升性能。
-XX:+DoEscapeAnalysis开启逃逸分析(JDK 6u23以上默认开启) -XX:-DoEscapeAnalysis 关闭逃逸分析 #标量替换基于分析逃逸基础之上,开启标量替换必须开启逃逸分析 -XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试) -XX:-EliminateAllocations 关闭标量替换 #锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析 -XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试) -XX:-EliminateLocks 关闭锁消除
五、总结
- 能在方法内创建对象,就不要再方法外创建对象。毕竟这是为了GC好,也是为了提高性能。
- 栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。
- 逃逸分析的效果只能在特定场景下,满足高频和高数量的小容量的变量分配结构,才是合适的。
参考资料: