【jvm】逃逸分析深入理解

本文详细介绍了Java中的逃逸分析技术,及其在优化内存分配和提高程序性能方面的作用。逃逸分析可以使得局部对象在栈上分配,减少垃圾回收的压力,同时还能实现锁消除和标量替换等优化。通过示例展示了开启和关闭逃逸分析对程序执行时间和内存使用的影响,强调了栈上分配和标量替换在减少堆内存占用和提升执行效率上的效果。

导图

在这里插入图片描述

1、问题引入——堆是分配对象存储的唯一选择吗?

一般认为,Java对象都是在堆上分配的。但是,有一种特殊情况,那就是 如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。如何将堆上的对象分配到栈,需要使用逃逸分析手段。

2、逃逸分析概述

  • 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
  • 通过逃逸分析,JavaHotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
    eg:
   // 对象gcTest就是没有发生逃逸的对象,那么就可以分配到栈上,随着方法的结束,栈空间就被移除。
    // 将gcTest设置为null,此时就没有任何的引用指向gcTest了,
    public void mymethod(){
        GCTest gcTest=new GCTest();
        gcTest=null;
    }
    // 对象逃逸,当外部调用此方法的时候,同样会指向sb
    public static StringBuffer createStringBuffer(String s1,String s2){
        StringBuffer sb=new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }

    //优化: 对象未逃逸,可以栈上分配
    public static String createStringBuffer1(String s1,String s2){
        StringBuffer sb=new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

如果将变量的定义放到程序外面,然后程序里面的方法对此变量进行初始化等操作,那么就肯定不会逃逸,如下:
在这里插入图片描述
在这里插入图片描述
有没有发生逃逸,我们就关注方法里面new 的对象到底有没有被外部引用,没有发生逃逸,可以栈上分配,发生逃逸只能堆上分配

所以这里我们给出一个结论:开发中能使用局部变量的,就不要定义在方法外部

3、参数设置

jdk6以后,虚拟机默认开启逃逸分析,我们通过参数-XX:+DoEscapeAnalysis显式开启逃逸分析,通过参数-XX: +PrintEscapeAnalysis 查看逃逸分析的结果

4、逃逸分析的作用

4.1 栈上分配
  • JIT编译器在编译期间根据逃逸分析的结果,发现如果一一个对象并没有逃逸出方法的话,就可能被|优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
  • 常见的栈上分配的场景
    分别是给成员变量赋值、方法返回值、实例引用传递
  • 没有开启逃逸分析,则所有对象都是在堆中去分配空间
    eg:
在这里插入代码片/**
 * 栈上分配测试
 *-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 */
public class jvmStackAllocation {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i <10000000 ; i++) {
            alloc();
        }
        System.out.println(System.currentTimeMillis()-start + "ms");
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private static  void alloc(){
        User user=new User();  //未发生逃逸
    }

    static class User{
        String userName;
    }
}

vm options

-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails

执行结果104ms,并且发生了垃圾回收
在这里插入图片描述

此时 将运行参数改为 -Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails,开启栈上分配,结果为10s,而且没有进行垃圾回收
在这里插入图片描述

4.1.1总结

栈上分配减少了垃圾回收频率,增强了代码的执行效率

4.2同步省略(锁消除)
  • 如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能。
  • 在动态编译同步代码块的时候,JIT编译器可以借助逃逸分析来判断同步代码块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步代码块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。锁消除和锁粗化
    在这里插入图片描述
4.3、分离对象或标量替换

有的对象可能不需要作为一个连续的内存结构,也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

标量(Scalar)是指一个无法再分解成更小数据的数据。如Java中的原始数据类型(基本数据类型)就是标量。
相对的,那么还可以分解的数据就叫聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量或者标量。
在JIT阶段,如果经过逃逸分析后发现,一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的成员变量来代替。 这个过程就是 标量替换

例如:

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

private static void alloc(){
	Point point = New Point(1,2);
}

class Point{
	private int x;
	private int y;
}

经过逃逸分析后发现,对象point 没有发生逃逸,则会进行标量替换。经过标量替换后,代码被优化成如下

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

private static void alloc(){
	int x = 1;
	int y = 2;
}

可以看到,point这个对象聚合量经过标量替换后,被替换成了两个它包含的标量x和y。

接下来我们能看出来它的好处了吧:它可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不在需要分配内存了

标量替换的JVM参数如下:

-XX:+EliminateAllocations 开启标量替换
-XX:-EliminateAllocations 关闭标量替换
-XX:+PrintEliminateAllocations 显示标量替换详情
4.3.1 测试标量替换效率
/**
 * 标量替换测试
 * -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="www.atguigu.com";
    }

    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");
    }
}

执行结果:发生了GC,并且时间为114ms
在这里插入图片描述
现在将VM options设置为:-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations,开启标量替换
执行结果:没有发生GC,并且时间为8ms
在这里插入图片描述
差距很明显吧!

5、逃逸分析小结

目前,逃逸分析技术依然不成熟,其根本原因是 无法保证逃逸分析的性能消耗一定高过它的消耗。虽然经过逃逸分析后可以做到标量替换、栈上分配和锁消除,但是逃逸分析本身也需要经历一系列复杂的分析,这其实是一个相对耗时的过程。
比如,如果经过逃逸分析后,发现所有对象都是逃逸的,那逃逸分析的过程就白白浪费了。
虽然技术并不成熟,但它还是即时编译器优化技术中一个重要的手段。

但是hotSpot虚拟机并没有用到栈上分配,所以可以明确所有的对象实例都是在堆上,但是上面的例子中确实都变的很快,而且没有发生GC,其实主要的效果还是基于标量替换, 就是对象转化为标量存在栈里,栈里还是没有对象,也就是说到目前为止,堆还是存储对象的唯一地方。

还有一个注意的地方:
在Server模式下,才可以启用逃逸分析,我们在64bit电脑上启动默认就是-server。

### 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`关闭它来进行对比测试,观察实际效果变化后再做决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值