堆(二)对象分配逃逸分析相关内容
提问:堆是分配对象的唯一选择吗?
初步结论:如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。栈上分配无序GC,随方法结束而弹栈,以此达到降低GC的回收频率和提升GC的回收效率的目的。
一、逃逸分析
对象是否发生逃逸,需要分析对象的动态作用域:
- 如果一个对象在方法中被定义后,对象只在方法内部使用,则被认为没有发生逃逸
- 如果一个对象在方法中被定义后,它被外部方法所引用,则被认为发生逃逸
- 如果快速判断是否逃逸,就要看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寄存器中。
1.栈上分配
JIT编译器在编译期间根据逃逸分析的结果,如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须GC了
常见的发生逃逸的场景:给成员变量赋值、方法返回值、实例引用传递
//-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
public class StackAllocation {
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");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User {
}
}
未开启逃逸分析,不进行栈上分配
开启逃逸分析后,进行栈上分配,减少GC
2.同步省略
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除
3.分离对象或标量替换
标量Scalar:是指一个无法在分解成更小的数据的数据。Java中的原始数据类型就是标量。
聚合量Aggregate:指那些还可以分解的数据,比如Java中对象就是聚合量,因为它可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来替代。这个过程就是标量替换
标量替换使用:-XX:-EliminateAllocations(-代表关闭,+代表开启)
//标量替换测试
//-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 = "aaa";
}
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");
}
}
未开启标量替换:
开启标量替换后:
三、对象逃逸分析总结
通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。Oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。虽然这项技术并不成熟,但它也是即时编译器优化技术中十分重要的一个手段
- 目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。