我们常说:在Java中,new出来的对象都是被分配在堆上,但是这个结论不是那么的绝对。
在Java SE 6u23版本之前,对象的创建都是在堆空间创建的。但在Java SE 6u23版本版本及之后,HotSpot中默认开启了逃逸分析,所以对象还可能存在栈上。
开启关闭逃逸分析:
执行java程序时,可以通过如下参数开启或者关闭"逃逸分析"
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
逃逸分析的定义
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。逃逸分析在Java即时编译器(JIT)编译Java源代码时使用。
通过逃逸分析算法可以分析出某一个方法中的某个对象是否会被其它方法或者线程访问到。如果分析结果显示某对象并不会被其它线程访问,则有可能在编译期间其做一些深层次的优化。
Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
对象的逃逸状态
逃逸分析将对象分为三种逃逸状态:
- 全局逃逸(GlobalEscape)
- 参数逃逸(ArgEscape)
- 没有逃逸(NoEscape)
1. 全局逃逸
对象的作用范围逃出了当前方法或者当前线程,如:
- 对象是一个静态变量
- 对象作为方法的返回值
- 复写了finalize()方法的类,该类的的对象都会被标记为全局逃逸状态并且一定会放在堆内存中
2. 参数逃逸
对象被作为方法参数传递或者被参数引用,但在调用过程中不会再被其它方法或者线程访问。这个对象标记为参数级别的逃逸。
3. 没有逃逸
对象只在方法内部使用,没有发生逃逸。
对于没有逃逸的这种对象,Java即时编译器会做出进一步的优化。
小结
对象逃逸可以分为两种:方法逃逸、线程逃逸
-
方法逃逸:对象逃出当前方法
对象在方法里面被定义后,它可能被外部方法所引用,如:
- 对象作为方法的返回值或对象的引用包含在返回值中
- 对象作为方法参数传递或者被参数引用
-
线程逃逸:对象逃出当前线程
对象可能被其它线程访问到
- 对象是一个静态变量
- 对象是一个可以在其它线程中访问的实例变量
逃逸分析优化方式
- 如果对象的逃逸状态是全局逃逸或者参数逃逸,则此对象必须被分配在堆内存中;
- 但是如果对象的逃逸状态是没有逃逸状态,则不一定被分配在堆内存中。
具体逃逸分析的优化有下面几种方式。
1. 锁消除
如果对象不会逃逸,这个对象就永远不会被其它方法或者线程访问到。
这时对象锁synchronized(object) 没有任何意义(因为在任何线程中,object都是不同的锁对象)。所以JVM会对上述代码进行优化,删除锁。
锁消除举例:
public void lockEliminate() {
User user = new User();
synchronized (user) {
System.out.println("执行同步代码块");
}
}
在lockEliminate() 方法中,对象user永远不会被其它方法或者线程访问到,因此user是不会逃逸对象,这就导致synchronized(user) 没有任何意义,因为在任何线程中,user都是不同的锁对象。
所以JVM会对上述代码进行优化,删除同步相关代码,如下:
public void lockEliminate() {
User user = new User();
System.out.println("执行同步代码块");
}
常见的锁消除场景:StringBuffer
public void lockEliminate() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("1");
stringBuffer.append("2");
System.out.println(stringBuffer.toString());
}
此时append方法上的synchronized锁会被消除
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
实际等同于
@Override
public StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
2. 栈上分配
如果对象不会逃逸,那么这个对象可能会被分配在栈内存上而非常见的堆内存上(对象分配消除)。
对象分配消除:是指将本该在"堆"中分配的对象,转化为由"栈"中分配。
上面为什么说的是可能呢?
因为Hotspot中采用的是解释器和编译器并行的架构,所谓的混合模式就是解释器和编译器搭配使用。
- 当程序启动初期,采用解释器执行(同时会记录相关的数据,比如函数的调用次数,循环语句执行次数),节省编译的时间。
- 在使用解释器执行期间,记录的函数运行的数据,通过这些数据发现某些代码是热点代码,采用编译器对热点代码进行编译,以及优化(逃逸分析就是其中一种优化技术)。
所以逃逸分析只在编译器进行编译才会有。
3. 标量替换
如果对象不会逃逸,分配到栈内存的时候,会进行标量替换。
栈中直接分配对象难度太大,需要修改JVM中大量堆优先分配的代码,因此在HotSpot中并没有真正的实现栈中分配对象的功能,取而代之的是一个叫做标量替换的折中办法。
标量和聚合量:
- 标量:基础类型和对象的引用可以理解为标量,它们不能被进一步分解
- 聚合量:能被进一步分解的量就是聚合量,对象就是聚合量,它可以被进一步分解成标量
将对象的成员变量分解为分散的标量,这就叫做标量替换。
这样,如果一个对象没有发生逃逸,那压根就不需要在"堆"中创建它,只会在栈或者寄存器上创建一些能够映射这个对象标量即可,节省了内存空间,也提升了应用程序性能。