Java编译器优化逃逸分析详解

通常面试官都会问:new出来的对象是不是一定都被分配在堆上?

在Java SE 6u23版本之前,对象在堆空上间创建。Java SE 6u23 及更高版本默认支持并启用逃逸分析,使得对象可能存在栈上。

接下来让我们一看看了解逃逸分析
1

什么是逃逸分析

逃逸分析(Escape Analysis)是一种技术,Java HotSpot 服务器编译器可以通过该技术分析新对象的使用范围并决定是否在 Java 堆上分配该对象。它与类型继承关系分析一 样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。

在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译:

第一段是通过javac命令把.java文件转换成.class文件。

第二段编译是把.class转换成机器指令的过程。

在第二编译阶段,传统的JVM的解释器(Interpreter)解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢很多。为了解决效率问题,引入了 JIT(即时编译) 技术。

引入了 JIT 技术后,Java程序仍然通过解释器进行解释执行,当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码”翻译成本地机器相关的机器码,并进行优化,然后再把翻译后的机器码缓存起来,以备下次使用。

逃逸分析就是属于JIT优化中一项内容。

基本原理与逃逸状态

分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部 方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访 问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线 程逃逸,称为对象由低到高的不同逃逸程度。

全局逃逸(GlobalEscape):对象逃逸出当前方法和线程。例如: 存储在静态字段中的对象、存储在转义对象的字段中或作为当前方法的结果返回的对象。


参数逃逸( ArgEscape):对象作为参数传递或由参数引用,但在调用期间不会全局逃逸。这个状态是通过分析被调用方法的字节码来确定的。

没有逃逸(NoEscape): 对象只在方法内部使用,没有发生逃逸。该对象是一个标量可替换对象,这意味着它的分配可以从生成的代码中删除。


逃逸分析优化方式

如果能证明一个对象不会逃逸到方法或线程之外(换句话说是别的方法或线程无法通过任何途径 访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化。

栈上分配(Stack Allocations)

在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是 Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。

在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所 占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。

栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换(Scalar Replacement)

若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据 就可以被称为标量。


如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java 中的对象就是典型的聚合量。

根据程序访问的情况,发现一个对象不会被外部访问的话,JIT优化就会把一个Java对象拆散,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。

public static void main(String[] args) {
    alloc();
}

private static void alloc() {
    User user = new User(18);
    System.out.println("age:" + user.age);
}

public class User {
    private int age;

    public User(int age) {
        this.age = age;
    }
}

以上代码中,user对象并没有逃逸出alloc方法,并且user对象是可以拆解成标量的。JIT就不会直接创建User对象,而是直接使用标量int age替代User对象。

以上代码,经过标量替换后,就会变成:

private static void alloc() {
   int age = 18;
   System.out.println("age:" + age);
}

以上例子将对象拆分后,除了可以让对象的成员变量在栈上 (栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可 以为后续进一步的优化手段创建条件。

标量替换可以视作栈上分配的一种特例,实现更简单(不用考 虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

同步消除(Synchronization Elimination)

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉,这一过程就叫同步消除,也称为锁消除。

同步消除举例:

public void eiminate() {
    User user = new User();
    synchronized (user) {
        System.out.println("执行同步代码块");
    }
}

在eiminate() 方法,任何线程中,user都是不同的锁对象,对象user永远不会被其它方法或者线程访问到,因此user是不会逃逸对象,这就导致synchronized(user) 没有任何意义。

所以在JIT编译阶段就会被优化掉。优化成:

public void eiminate() {
    User user = new User();
    System.out.println("执行同步代码块");
}

逃逸扩展及开启关闭

关于逃逸分析的研究论文早在1999年就已经发表,但直到JDK 6,HotSpot才开始支持初步的逃逸 分析,而且到现在这项优化技术尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是逃逸分析 的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗。

C和C++语言里面原生就支持了栈上分配(不使用new操作符即可),而C#也支持值类型,可以很 自然地做到标量替换(但并不会对引用类型做这种优化)。在灵活运用栈内存方面,确实是Java的一 个弱项。

-XX:+DoEscapeAnalysis : 表示开启逃逸分析

-XX:+PrintEscapeAnalysis来查看分析结果。

有了逃逸分析支持之后,用户可 以使用参数

-XX:+EliminateAllocations 开启标量替换,

+XX:+EliminateLocks 开启同步消 除,

-XX:+PrintEliminateAllocations 查看标量的替换情况。

关闭逃逸分析

-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析。

在这里插入图片描述
松下问童子,言师采药去。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三省同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值