Java 逃逸分析

在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,第一段是把.java文件转换成.class文件。第二段编译是把.class转换成机器指令的过程。

第一段编译就是javac命令。

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

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

由于关于JIT编译和热点检测的内容,我在深入分析Java的编译原理中已经介绍过了,这里就不在赘述,本文主要来介绍下JIT中的优化。JIT优化中最重要的一个就是逃逸分析。

逃逸分析

关于逃逸分析的概念,可以参考对象和数组并不是都在堆上分配内存的。一文,这里简单回顾一下:

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

例如以下代码:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。

使用逃逸分析,编译器可以对代码做如下优化:

一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。这样将大大减少GC的工作,提升性能。这项功能依托于下面的标量替换实现:

三、标量替换。可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。

Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

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

-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

同步消除

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除

-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析

如以下代码:

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

所以,在使用synchronized的时候,如果JIT经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除。

标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭标量替换
标量替换基于分析逃逸基础之上,开启标量替换必须开启逃逸分析

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

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

private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}

以上代码中,point对象并没有逃逸出alloc方法,并且point对象是可以拆解成标量的。那么,JIT就会不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。

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

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}

可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。

标量替换为栈上分配提供了很好的基础。其实,Java中并没有在栈上分配提供实现,所谓的栈上分配就是通过标量替换实现的。

栈上分配

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。

关于栈上分配的详细介绍,可以参考对象和数组并不是都在堆上分配内存的。

这里,还是要简单说一下,其实在现有的虚拟机中,并没有真正的实现栈上分配,在对象和数组并不是都在堆上分配内存的。中我们的例子中,对象没有在堆上分配,其实是标量替换实现的。

逃逸分析并不成熟

关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是他也是即时编译器优化技术中一个十分重要的手段。

 

关于测试已经命令实践,请看简书上这位的文献:https://www.jianshu.com/p/04fcd0ea5af7

<think>嗯,用户想了解Java中的逃逸分析,包括它的概念、原理和应用。首先,我得回忆一下逃逸分析的定义。根据引用[1],逃逸分析是JVM在编译时(比如JIT阶段)分析对象的作用域,判断对象是否逃逸出方法或线程。如果对象没有逃逸,JVM可以优化内存分配,比如在栈上分配而不是堆上,或者进行锁消除和标量替换。 接下来,我需要整理逃逸分析的三个主要优化策略:栈上分配、锁消除和标量替换。引用[4]提到对象的三种逃逸状态:全局级别、参数级别和无逃逸。无逃逸的对象可以进行栈分配,减少堆的压力,这对性能有帮助。而锁消除,比如同步代码块中的对象如果没有逃逸,JVM会去掉同步操作,提升效率。标量替换则是将对象拆解成基本类型变量,减少对象创建的开销。 然后,用户可能想知道逃逸分析的原理。引用[2]和[3]指出逃逸分析JIT阶段进行,分析对象的作用域。如果对象不会逃逸到方法或线程外,就可以应用优化。例如,局部变量如果没有被外部引用,就可能被优化。 应用场景方面,引用[5]提到了JVM参数设置,比如启用逃逸分析的选项。实际开发中,编写局部作用域的对象,避免不必要的逃逸,比如不返回对象或将其赋值给类变量,可以触发优化。比如在循环内创建对象,如果对象没有逃逸,JVM可能会优化掉这些对象的堆分配。 不过,逃逸分析也有局限性,比如分析过程复杂,可能增加编译时间,且不是所有情况都能优化。比如引用[3]提到对象并非全在堆上分配,但具体效果依赖JVM实现和代码结构。 用户可能的问题包括如何判断对象是否逃逸、如何启用逃逸分析、优化失效的情况等。需要结合引用中的信息来回答,比如使用JVM参数,或者检查代码结构避免对象逃逸。 最后,确保回答结构清晰,分点解释概念、原理、应用和注意事项,并引用正确的来源。还要生成相关问题,帮助用户深入理解。</think>### Java逃逸分析的概念及作用 #### 一、逃逸分析的定义 **逃逸分析**是JVM在编译阶段(如JIT即时编译)对代码进行的一种优化技术,通过分析对象的生命周期和作用域,判断对象是否会“逃逸”出方法或线程的范围。若对象未逃逸,JVM可应用多种优化策略以提升性能或节省内存[^1][^2][^4]。 --- #### 二、逃逸分析的原理 1. **逃逸状态分类** - **全局逃逸**:对象可能被其他方法或线程访问(例如作为方法返回值或赋值给静态变量)。 - **参数逃逸**:对象作为参数传递给其他方法,但未进一步逃逸到更外层。 - **无逃逸**:对象仅在当前方法内部使用,生命周期与方法执行周期一致[^4]。 2. **分析过程** JVM通过静态分析判断对象的逃逸状态。例如,局部变量若未传递到外部或被其他线程引用,则标记为“无逃逸”[^2][^3]。 --- #### 三、逃逸分析的优化应用 1. **栈上分配(Stack Allocation)** - 若对象无逃逸,JVM将其分配在栈帧中而非堆上,减少垃圾回收压力。 - 示例:循环中创建临时对象,若未逃逸则直接在栈上分配。 2. **锁消除(Lock Elision)** - 若同步代码块中的对象未逃逸(即线程私有),JVM会移除无意义的同步锁[^4]。 - 示例:`synchronized(new Object()){...}` 会被优化为普通代码块。 3. **标量替换(Scalar Replacement)** - 将对象拆分为多个基本类型变量(标量),直接在栈上存储,避免创建完整对象[^4]。 - 示例:`Point p = new Point(x, y);` 可能被替换为两个局部变量`int x, int y`。 --- #### 四、实际开发中的优化建议 1. **启用逃逸分析** - 默认已启用(JDK 7+),可通过JVM参数调整: ```bash -XX:+DoEscapeAnalysis # 启用逃逸分析 -XX:+EliminateLocks # 启用锁消除 ``` [^5] 2. **编码注意事项** - 尽量缩小对象作用域(如局部变量优于成员变量)。 - 避免将局部对象暴露给外部(如不返回对象或赋值给全局变量)。 --- #### 五、局限性 1. **分析成本高**:复杂的逃逸分析可能延长JIT编译时间。 2. **优化条件严格**:仅适用于无逃逸对象,且依赖JVM实现细节[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值