🍟
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有种特殊情况,那就是如果经过 逃逸分析( Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成 栈上分配。这是最常见的堆外存储技术。
逃逸分析
判断是否会将堆上的对象分配到栈上去就需要逃逸分析。
逃逸分析基本概念
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中
举例
/**
* 逃逸分析
*
* 如何快速的判断是否发生了逃逸分析,就看new的对象实体是否有可能在方法外被调用。
*/
public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//如果当前的obj引用声明为static仍然会发生逃逸。
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()同样会发生逃逸,因为这个对象本身就是从
// 外面传进来的。
}
}
参数设置
结论:为了避免发生逃逸。开发中能用局部变量的就不要在方法外定义。
逃逸分析之代码优化
使用逃逸分析,编译器可以对代码做如下优化:
- 栈上分配。将堆分配转化为栈分配。
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储CPU寄存器中。
栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。
分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
同步省略
线程同步的代价是相当高的,同步的后果是降低并发性和性能,所以在可以不用同步的情况下就给它省略掉。
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
示例:
public class SynchronizedTest {
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
}
因为hollis没有发生逃逸,所以JIT编译器会取消这部分代码的同步/锁。
就会变成如下代码:
public class SynchronizedTest {
public void f() {
Object hollis = new Object();
System.out.println(hollis);
}
}
注意:
分离对象/标量替换
标量( Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量( Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象如果没有发生逃逸,那么经过JIT优化,就会把这个对象拆解成若干个标量来代替。这个过程就是标量替换。
示例
/**
* 标量替换测试
* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class ScalarReplace {
public static class User {
public int id;
public String name;
}
public static void alloc() {
User u = new User();//未发生逃逸
u.id = 5;
u.name = "www";
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
}
}
上述代码在主函数中进行了1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就必然会发生GC。
根据我们设置的参数,堆的大小远远小于1.5G.但程序并未发生OOM,就是因为发生了标量替换
标量替换的好处:可以大大减少堆内存的占用。
因为不需要创建对象,就不再需要分配堆内存了。
参数设置:
逃逸分析小结
逃逸分析并不是十分成熟。
其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,
如果经历的一轮逃逸分析,发现所有的对象都逃逸了,那就白白浪费了性能去做这个分析。