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 调优、后端面试必考知识点。

相关推荐
步行cgn35 分钟前
Spring Boot 启动报错:MissingServletWebServerFactoryBean 的排查与解决
java·spring boot·后端
步行cgn1 小时前
Spring Boot 项目打包部署详解
java·spring boot·后端
我的xiaodoujiao2 小时前
Django 基础知识详细图文教程 4-Django 视图定义与使用
开发语言·数据库·后端·测试工具·django·sqlite
Apifox2 小时前
Apifox 8 月更新|调试、权限与协作体验持续优化
前端·后端·测试
kyson_2 小时前
后端登录凭证设计:Session-Cookie、Redis-Token 与 JWT
后端
对象存储与RustFS2 小时前
RustFS 后台扫描器与自愈调参:五档速度、位腐检测周期与并发上限
后端·rust·开源
天天喝旺仔2 小时前
Docker 镜像瘦身实战:多阶段构建把体积缩小 90%
运维·后端·ci/cd·docker·云原生·容器·性能优化
Lost of 程序猿2 小时前
ASP.NET Core Saga 分布式事务深度实战:备件采购跨服务长流程,如何保证“要么全成,要么全回“
分布式·后端·asp.net
卷无止境2 小时前
Coding Agent 里的上下文 Compact,到底在压缩什么
后端·python
爱勇宝2 小时前
公司没给活干,却因为员工看手机把人开了:法院判赔11万元
前端·后端·程序员