new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析

逃逸分析(Escape Analysis)是一种静态程序分析技术,主要用于判定对象的可见范围(Visibility)与生命周期(Lifetime)。该技术是现代即时编译器实现局部化优化、提升内存使用效率、降低同步成本的基础。

通俗来说,逃逸分析的核心在于回答这样一个问题:某个对象是否可能"逃逸"出它所创建的方法或线程作用域?

逃逸分析的结果通常分为三种情形。

1)未逃逸(No Escape):对象完全局限在当前方法内,既未作为返回值,也未传递到其他线程或方法。

2)方法逃逸(Method Escape):对象作为参数传递到其他方法中,虽然不一定跨线程访问,但由于编译器无法确定外部方法的副作用,因此仍视为潜在逃逸。

3)线程逃逸(Thread Escape):对象的引用被赋值给共享变量,或作为任务传递给其他线程。这类对象无法进行逃逸相关优化,必须保留其线程安全保障。

下面代码的是对象未逃逸的例子:

java 复制代码
// add方法中创建了一个名为NonEscapeObject的对象。
// 这个对象仅在add方法中使用,用于计算两个整数的和。
// 这个对象没有作为方法的返回值、赋值给全局变量或作为参数传递给其他方法。
// 因此它被认为是未逃逸的。
int add(int a, int b) {

  NonEscapeObject o = new NonEscapeObject(a, b);

  return obj.getX() + obj.getY();
}

class NonEscapeObject {

    private int x;

    private int y;
}

基于逃逸分析的信息,即时编译器可以执行一些优化,例如同步锁消除(Synchronization Elimination)、标量替换(Scalar Replacement)和栈上分配(Stack allcotion)。

同步锁消除

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

java 复制代码
// 由于obj没有逃逸出doSomething()方法的范围,编译器可以进行逃逸分析并确定该对象不会被其他线程访问。
// 在逃逸分析确定obj对象不会逃逸的情况下,编译器可以消除对该对象的同步锁操作。
void doSomething() {

    Object obj = new Object();

    synchronized (obj) {

        // 对obj进行一些操作

        // ...
    }
}

标量替换

标量(scalar)是指一个无法再分解成更小的数据的数据。Java 中的基本数据类型就是标量。相对的Java 中的对象就是聚合量(Aggregate),因为它可以分解成其他聚合量和标量。

如果经过逃逸分析,发现一个对象并没有逃逸出方法和线程,那么就可以将这个对象视为一组标量值。这样,Java虚拟机就可以将这个对象的所有字段视为局部变量,从而在栈上分配这些局部变量,而不是在堆上分配整个对象,这样可以减少堆内存的占用。

java 复制代码
void test() {

   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对象,包含x和y两个字段。如果经过逃逸分析,发现这个Point对象并没有逃逸出方法,那么Java虚拟机就可以将这个Point对象视为两个独立的标量值x和y,然后在栈上分配这两个值,而不是在堆上分配整个Point对象。

java 复制代码
void test() {
    int x = 1;

    int y = 2;

    System.out.println("point.x = " + x + "; point.y=" + y);
}

栈上分配

Java的对象是在堆上分配的,Java虚拟机对堆内存的垃圾对象回收是一个耗时的过程。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁,垃圾收集系统的压力将会小很多。

虽然逃逸分析理论上支持将非逃逸对象直接分配到栈上,从而避免堆内存开销与垃圾回收成本,但如HotSpot虚拟机并未真正实现物理意义上的栈上分配。原因在于:在支持线程抢占、嵌套调用、异常恢复与栈帧迁移(如逃逸到堆)等复杂运行时语义的情况下,栈上对象生命周期管理的正确性将变得异常困难,容易引发并发可见性等问题。因此,Hotspot虚拟机并没有进行实际的栈上分配,而是使用了标量替换这一技术。

尽管逃逸分析为即时编译器带来了多种激进优化的可能,但它本身也是一项计算复杂度较高的静态分析技术。在分析过程中,编译器需要对对象的引用路径进行全程追踪,判断其是否会被其他线程访问、是否会通过方法返回或赋值跨出当前作用域。特别是在存在复杂控制流、间接调用或反射的情况下,分析准确性与代价都将急剧上升。

这种计算成本并非微不足道:在代码编译时长与运行时性能收益之间,并不总是呈现出正向关系。在某些边缘场景中,逃逸分析所带来的优化甚至可能因分析开销过大、代码形态不佳(例如过度拆箱、短生命周期对象)而无法收回性能投入。因此,Java虚拟机会采用热点代码触发机制,仅对高频路径进行逃逸分析,以期在收益与成本之间实现动态平衡。

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

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!

相关推荐
前端缘梦1 天前
解锁webpack核心技能(三):从源代码到打包产物编译过程的原理指南
webpack·编译原理·前端工程化
poemyang1 天前
解锁硬件潜能:Java向量化计算,性能飙升W倍!
java虚拟机·编译原理·jit·向量化计算·smid
poemyang2 天前
Java编译器优化秘籍:字节码背后的IR魔法与常见技巧
java虚拟机·编译原理·ir·即时编译器
poemyang3 天前
“代码跑着跑着,就变快了?”——揭秘Java性能幕后引擎:即时编译器
java·java虚拟机·编译原理·jit·即时编译器
poemyang4 天前
“同声传译”还是“全文翻译”?为何HotSpot虚拟机仍要保留解释器?
java·java虚拟机·aot·编译原理·解释执行
漂流瓶jz7 天前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理
poemyang7 天前
a+b=c,处理器一步搞定,Java虚拟机为啥要四步?
java·java虚拟机·java字节码
poemyang8 天前
Hello World背后藏着什么秘密?一行代码看懂Java的“跨平台”魔法
java·java虚拟机·编译原理·java字节码
SHERlocked9325 天前
C++ 中的编译和链接
c++·面试·编译原理