Java 的1 亿次对象创建:JVM 开启 / 关闭逃逸分析,GC 性能差距巨大

前言

很多人不解:逃逸分析开关,为何会让GC性能差距巨大?

很多人误以为Java对象只能堆分配、依赖GC回收。实际上,JVM可通过即时编译优化,让方法内临时对象无需入堆,是面试和调优的核心知识点。核心分为三点:逃逸分析、标量替换、栈上分配

本文结合实测代码与JVM参数,透彻讲解该核心优化机制。


一,对象内存分配流程图


二、对象栈上分配

Java 对象默认堆分配、靠 GC 回收,大量临时对象会加剧 GC 开销。JVM 经逃逸分析后,会将未逃逸的对象分配在栈上,随栈帧自动销毁,降低 GC 压力。

1、对象逃逸分析

  • 即时编译过程中JVM可能会对我们的代码最一些优化,比如对象逃逸分析等
  • 分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用 ,例如作为调用参数传递到其他地方中

1.1、🌰举个例子

csharp 复制代码
    // 不逃逸:可栈上分配
    public void add() {
        Test t = new Test(1, 2); // 仅方法内使用
        int sum = t.x + t.y;
    }

    // 方法逃逸:不可栈上分配
    public Test create() {
        Test t = new Test(1, 2);
        return t; // 返回给调用方,逃出当前方法
    }

    // 线程逃逸:不可栈上分配
    public static Test globalTest;
    public void init() {
        globalTest = new Test(1, 2); // 赋值给静态变量,跨线程可见
    }
  • add中的 Test 对象仅方法内使用,方法结束后无有效引用。
  • create 返回 Test 对象至外部,调用方持有引用,对象生命周期不可控。
  • init 将 Test 对象赋值静态全局变量,多线程均可访问,产生线程逃逸,生命周期不可控。

仅像 add方法中这种只在方法内使用、方法执行结束后即刻失效的对象,支持分配至栈内存,方法退出时该对象会随当前栈帧一同自动回收。

⚠️注意:

✔️ JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换 优先分配在栈上(栈上分配 )

✔️JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

2、HotSpot 的实际实现:标量替换

对于未逃逸的对象JIT 编译器不会创建完整的对象结构(包含对象头、实例数据的连续内存),而是将对象拆解为一个个独立的标量 (基本数据类型、引用字段),直接把这些标量作为局部变量,分配在栈帧的局部变量表中,甚至直接放入 CPU 寄存器

标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等)

ini 复制代码
    // 优化前:创建完整Point对象,分配在堆
    void method() {
        Test t = new Test(1, 2);
        int d = t.x + t.y;
    }

    // 优化后(标量替换):不创建对象,直接用栈上局部变量
    void method() {
        int x = 1;
        int y = 2;
        int d = x + y;
    }

⚠️ 需要特别说明一个关键细节:

✔️Oracle HotSpot 虚拟机并没有实现 "完整对象直接放入栈中" 的栈上分配,而是通过更激进的标量替换(Scalar Replacement)达到等效甚至更优的效果,这也是业内常说的 "HotSpot 栈上分配" 的真实实现方式。

3、 标量替换

3.1、 🌰举个例子:1 亿次对象创建

arduino 复制代码
package com.nl;

public class TestStack {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test();
        }
        long end = System.currentTimeMillis();
        System.out.println("结束时间:" + (end - start));
    }

    private static void test() {
        Test t = new Test(1, 2);
    }

    public static class Test {
        private int x;
        private int y;

        public Test(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}

3.2、使用如下参数GC 次数大幅降低

开启逃逸分析

ruby 复制代码
-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

运行结果:仅触发 1 次 Young GC,程序总耗时 4ms。

csharp 复制代码
[GC (Allocation Failure)  4096K->1140K(15872K), 0.0006532 secs]
结束时间:4

3.2、使用如下参数都会发生大量GC

关闭逃逸分析

ruby 复制代码
-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

运行结果:仅触发 N 次 Young GC,程序总耗时 308ms。

csharp 复制代码
[GC (Allocation Failure)  4096K->1135K(15872K), 0.0007905 secs]
......
[GC (Allocation Failure)  5431K->1335K(15872K), 0.0002849 secs]
结束时间:308

⚠️注意:

✔️逃逸分析参数:-XX:+DoEscapeAnalysis

✔️栈上分配依赖于逃逸分析和标量替换

✔️**开启逃逸分析后,JIT 编译器可对未逃逸的临时对象执行标量替换(等效栈上分配优化) **,对象随方法栈帧出栈直接销毁,不占用堆内存。这大幅降低了年轻代的内存分配压力,GC 频次显著减少,程序执行效率提升近百倍。

4、允许标量替换优化条件

未逃逸的数组:

  • 长度 ≤ 64:可以执行标量替换优化
  • 长度 > 64:直接判定为优化收益过低,放弃标量替换,正常在堆上分配

三、面试题

1、Java 对象默认分配在堆,为什么要做栈上分配优化?有什么好处?

大量短期临时对象全部堆分配会频繁触发 GC,增加 STW 停顿、损耗性能。

逃逸分析 + 标量替换实现等效栈分配,优势:

  • 不占用堆内存,无需 GC 回收对象;
  • 方法执行完毕,栈帧出栈时局部变量自动销毁;
  • 大幅减少年轻代分配失败触发的 Minor GC,提升运行速度。

2、HotSpot 虚拟机真的会把完整对象分配到栈内存吗?底层实现原理是什么?

HotSpot 并没有直接将完整对象存入栈,业界所说的 "栈上分配" 本质是标量替换。原理:

  • 标量指无法拆分的数据(基本类型、引用);
  • 若对象无逃逸,JIT 拆解对象所有成员变量,转为独立标量,存放在栈帧局部变量表甚至 CPU 寄存器;
  • 不再创建堆上对象,达到和栈分配完全一致的效果。
    配套开启标量替换参数:-XX:+EliminateAllocations

四、总结

逃逸分析是 JVM 内置免费性能优化,日常开发无需手动开启,了解底层原理能帮助我们写出更利于 JIT 优化的代码,同时也是 JVM 调优、后端面试必考知识点。

相关推荐
tangdou3690986551 小时前
AI真好玩系列-2分钟快速了解DeepAgents | Quick Guide to DeepAgents in 2 Minutes
前端·javascript·后端
神奇小汤圆2 小时前
面试官:MySQL 为什么要是使用 MVCC?原理是什么?
后端
像我这样帅的人丶你还2 小时前
Java 后端详解(五):Redis 缓存
java·后端·全栈
玉宇夕落2 小时前
别再死磕 Prompt 了!上下文工程 (Context Engineering) 的简单学习
后端
用户34232323763172 小时前
定时器与 PWM 输出详解
后端
Jason_chen3 小时前
Linux 6.2 CAN/CANFD机制详解
后端
Apifox4 小时前
Apifox 6 月更新|Apifox CLI 全面升级、导入导出优化、OAuth 2.0 支持自动刷新令牌
前端·后端·测试
悟空瞎说4 小时前
NestJS 接口设计避坑:摒弃万能用户更新接口,落地单一职责与最小权限原则
后端·nestjs
smallyoung4 小时前
Spring AI 2.0 VectorStore实战:从原理到RAG落地
人工智能·后端