1 概述
- 如何将堆上的对象分配到栈,需要使用逃逸分析手段
- 这是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
- 通过逃逸分析,java hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
- 逃逸分析的基本行为就是分析对象动态作用域:
- 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
- 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其他方法中
- 没有发生逃逸的对象,则可以分配到栈上,随着方法执行结束,栈空间就被移除
- 相关参数
- -server : 启用server模式,因为只有在server模式下,才可以启用逃逸分析
- -XX:+DoEscapeAnalysis : 启动逃逸分析
- -XX:+EliminateAllocations : 开启标量替换(默认打开),允许将对象打散分配到栈上
2 举例
// 未发生逃逸 public void my_method(){ V v = new V(); // do... v = null; } // 发生逃逸 public static StringBuffer createStringBuffer(String s1,String s2){ StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } // 上述代码要想StringBuffer sb 不逃出方法,可以这样写 public static String createStringBuffer(String s1,String s2){ StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
package com.atguigu.java2; /** * 逃逸分析 * * 如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。 * @author shkstart * @create 2020 下午 4:00 */ 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()同样会发生逃逸 } }
3 JIT对代码的优化
使用逃逸分析,编译器可以对代码做如下优化
- 栈上分配: 将堆分配转化为栈分配,如果一个对象在子程序中被分配,要使用指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配.
- JIT编译器在编译期间根据逃逸分析的结果,如果发现一个对象并没有逃逸出方法的话,就可能被优化成栈上分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需垃圾回收了
- 常见的栈上分配场景
- 给成员变量赋值,方法返回值,实例引用的传递
- 同步省略(消除): 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作,可以不考虑同步,消除同步代码
- 线程同步的代价是相当高的,同步的后果是降低并发性,和性能
- 在动态编译同步块的时候,JIT编译器,可以借助逃逸分析来判断同步块所使用的锁对象,是否只能够被一个线程访问而没有发布到其他线程,如果没有,那么JIT编译器在编译这个同步块的时候,就睡取消对这部分代码的同步,这样就能大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
- 分离对象或标量替换: 有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中.
- 标量(Scalar) 是指一个无法再分解成更小的数据结构,java中的原始数据类型就是标量, 相对的哪些可以再分解的数据叫做聚合量(Aggregate),java中的对象就是聚合量,因为他可以分解成其他聚合量和标量,在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替,这个过程就是标量替换
-
class Point{ private int x; private int y; } private static void alloc() { Point point = new Point(1, 2); System.out.println("point.x = " + point.x + "; point.y = " + point.y); } // 以上代码经过标量替换后,变成 private static void alloc() { int x = 1; int y = 2; System.out.println("point.x = " + x + "; point.y = " + y); }
4 结论
- 如何快速的判断是否发生了逃逸分析,就看new的对象实体是否有可能在方法外被调用。
- 开发中能使用局部变量的,就不要使用在方法外定义