深入分析JVM逃逸分析对性能的影响

本文深入探讨了逃逸分析的概念及其实现方式,包括方法逃逸、栈上分配、同步消除和标量替换等内容,并通过实验对比了开启和关闭逃逸分析对程序性能的影响。

逃逸分析(Escape Analysis)

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

方法逃逸的几种方式如下:

public class EscapeTest {
    public static Object obj;
    public void globalVariableEscape() {  // 给全局变量赋值,发生逃逸
        obj = new Object();
    }
    public Object methodEscape() {  // 方法返回值,发生逃逸
        return new Object();
    }
    public void instanceEscape() {  // 实例引用发生逃逸
        test(this); 
    }
}

栈上分配

栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。

同步消除

线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(锁和锁块内的对象不会逃逸出线程就可以把这个同步块取消)

标量替换

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

栈上分配深入分析

public class OnStackTest {
    public static void alloc(){
        byte[] b=new byte[2];
        b[0]=1;
    }
    public static void main(String[] args) {
        long b=System.currentTimeMillis();
        for(int i=0;i<100000000;i++){
            alloc();
        }
        long e=System.currentTimeMillis();
        System.out.println(e-b);
    }
}

-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析

开启逃逸分析,执行的时间为4毫秒。如下图:
开启逃逸分析

关闭逃逸分析,执行的时间为618毫秒,并且伴随的大量的GC日志信息。如下图:
关闭逃逸分析

**通过上面开启和关闭逃逸分析:
开启逃逸分析,对象没有分配在堆上,没有进行GC,而是把对象分配在栈上。
关闭逃逸分析,对象全部分配在堆上,当堆中对象存满后,进行多次GC,导致执行时间大大延长。堆上分配比栈上分配慢上百倍。**

即时编译器(Just-in-time Compilation,JIT)
1、使用client编译器时,默认执行为1500次才认为是热代码;
2、使用server编译器时,默认执行为10000次才认为是热代码;
上面的例子开启逃逸分析后,并不是所有的对象都直接在栈上分配,而是通过JIT分析此代码是热代码,才进行异步编译成本地机器码,并通过逃逸分析,把对象分配到栈上。(如果是server编译器:在前10000次循环和编译成本地机器码这段时间,对象都会在堆中分配对象,编译成本地机器码后才会在栈上分配)

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

关闭标量替换
标量替换
这次我们打开逃逸分析,并且把标量替换功能关闭,我们发现对象又分配到堆里面了,并执行了多次GC。由此可以看出java中没有实现真正意义上的栈上分配,而是通过标量替换来实现栈上分配的。

锁消除深入分析

把上面的OnStackTest代码稍微修改了下,加了一个同步块。默认数组长度大于64的是不会在栈上分配的,我们都以堆上分配为例来测试锁消除带来的影响。

public class OnStackTest {
    public static void alloc(){
        byte[] b=new byte[65];
        synchronized (b) {  //同步代码块
            b[0]=1;
        }
    }
    public static void main(String[] args) throws IOException {
        long b=System.currentTimeMillis();
        for(int i=0;i<100000000;i++){
            alloc();
        }
        long e=System.currentTimeMillis();
        System.out.println(e-b);
    }
}

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

开启锁消除
开启锁消除
关闭锁消除
关闭锁消除

开启锁消除执行的时间为1807毫秒
关闭锁消除执行的时间为3801毫秒
通过开启和关闭锁消除我们可以看到性能最少提升1倍以上。

本人简书blog地址:http://www.jianshu.com/u/1f0067e24ff8    
点击这里快速进入简书

### JVM 逃逸分析概述 JVM中的逃逸分析是一种编译期优化技术,用于判断对象是否会逃逸出当前线程或方法的作用域。通过这种分析,JDK能够做出一些优化决策来提高性能,比如分配、同步消除等[^4]。 ### 逃逸分析引发内存泄漏的原因 当对象未正确处理其生命周期时,在某些情况下即使启用了逃逸分析也可能间接造成内存泄漏: - **错误的对象共享**:如果程序逻辑设计不当,使得原本应该局部化的对象意外地被其他部分持有引用,则这些对象无法及时回收,从而形成潜在的内存泄露风险。 - **误判为不逃逸的情况**:对于复杂的数据结构操作,尤其是涉及多线程环境下的并发访问模式下,可能存在难以精确判定是否发生逃逸的情形。此时可能会导致不必要的堆内存在长时间得不到释放而累积成内存泄漏现象。 ### 解决方案 针对上述由逃逸分析可能带来的问题,可以从以下几个方面着手解决: #### 合理调整GC策略与参数配置 适当调节垃圾收集器的选择及其相关参数可以帮助更好地管理应用程序内的资源消耗情况。例如启用G1 GC并合理设定初始和最大堆大小(-Xms,-Xmx),这有助于维持稳定的运行状态,防止由于频繁Full GC造成的性能瓶颈以及由此产生的隐含内存泄漏隐患[^1]。 ```bash java -XX:+UseG1GC -Xms512m -Xmx4g MyApplication ``` #### 审查代码逻辑确保无异常引用保留 仔细审查源码中涉及到对象创建及传递的部分,特别是那些跨作用范围使用的实例变量或者静态成员字段。确认它们不会无意间延长目标对象的生命周朗,进而阻碍正常的垃圾回收过程。 #### 利用工具辅助诊断排查 借助专业的性能剖析工具如VisualVM, MAT(Memory Analyzer Tool)等对正在执行的应用进程实施监控跟踪,定位具体引起内存占用过高的区域,并据此采取针对性措施加以改进。 #### 正确理解并运用逃逸分析特性 虽然现代版本Java已经默认开启此功能,但在特定场景下调优仍有必要深入了解该机制的工作原理。必要时可通过显式指定`-XX:-DoEscapeAnalysis`关闭它来进行对比测试,观察实际效果变化后再做决定。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值