首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java的对象一定是在堆上分配的嘛?谁这么说就直接用“逃逸分析”反驳他!

Java的对象一定是在堆上分配的嘛?谁这么说就直接用“逃逸分析”反驳他!

作者头像
程序员牛肉
发布2025-01-22 09:25:35
发布2025-01-22 09:25:35
2260
举报

大家好,我是程序员牛肉。

之前在和朋友聊天的时候,他突然问我什么是“逃逸分析”。说实话当时我还真不太能完整的讲出什么是逃逸分析。这玩意虽然我看八股的时候经常遇见,但之前还真没专项学习过。因此我们今天来完整的介绍一下什么是逃逸分析。

在正式的介绍逃逸分析之前,我们先来看一段代码:

代码语言:javascript
复制
package org.example;

public class Maintest {
    public static void main(String[] args) {
        while (true) {
            Integer integer = new Integer(111111111);
        }

    }
}

这段代码的逻辑很简单:就是不断的去创建一个值为111111111的integer对象。

[这里考考你们:为什么在创建intege对象的时候,我们的值要是“111111111”这种大数字,而不是随便给个1或者2?]

而这些对象都会被分配到堆中,使得堆内存很快被占满而触发GC。为了让这一过程更加明显,我们需要手动的设置一下JVM参数,使得GC过程被打印到控制台中可视化,以及通过调整堆大小的方式,使得更快的触发GC。对应的JVM参数为:

代码语言:javascript
复制
-Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis
  • -Xmx10m 设置 JVM 堆的最大内存为 10MB。这意味着在 Java 程序运行过程中,JVM 可以使用的最大内存量为 10MB。如果程序试图分配超过这个大小的内存,可能会抛出 OutOfMemoryError 异常。
  • -Xms10m 设置 JVM 堆的初始内存为 10MB。JVM 启动时,堆内存会被初始化为这个大小。通常建议将 -Xms-Xmx 设置为相同的值,以避免在运行时频繁调整堆内存大小,从而提高性能。
  • -XX:+PrintGC 这是一个 JVM 的诊断参数,开启后 JVM 会在进行垃圾回收时将相关信息打印到标准输出。这些信息有助于开发者了解垃圾回收的频率、回收的内存量等,从而对应用程序的内存使用情况进行分析和调优。
  • -XX:-DoEscapeAnalysis 关闭逃逸分析。逃逸分析是 JVM 的一项优化技术,它可以分析对象的作用域,判断对象是否会逃逸出方法或线程。如果对象不会逃逸,JVM 可以对其进行一些优化,如栈上分配、标量替换等。关闭此选项会禁用这些优化。

我们可以在下面的视频中观看这段代码的运行结果:

在这段代码的运行过程中,由于我们搭建了一个死循环来不断的创建对象加入到堆内存中,导致JVM的堆内存被快速挤占,频发引发GC。照着这样整,服务就离瘫痪不远了。

因此JDK 官方想到:我们可以分析对象的作用域,对于 像是上述代码中integer 这种仅在 while 循环内部使用,没有被返回给调用者、存储到全局变量或传递到其他线程中的对象,就可以将 integer 对象直接分配在栈上,而不是堆上。

这样一来,当每次循环结束时,该对象所占用的栈空间会随着栈帧的弹出而自动释放,无需等待垃圾回收器来处理,大大减轻了堆内存的压力,也减少了 GC 的频率。

我们可以看一看在开启逃逸分析之后这个代码的运行情况:

[JDK6以后,逃逸分析就默认开启了。因此我们只需要在上面提到的JVM的配置参数中删除之前配置的“-XX:-DoEscapeAnalysis”就可以。]

我们可以看到在开启了逃逸分析之后,我们的integer对象并不会被频繁的创建在堆内存上,而是存储在栈空间上,随着栈帧的弹出而自动释放,无需等待垃圾回收器来处理,大大减轻了堆内存的压力,也减少了 GC 的频率。

其实通过这个实例。我们就能够大致理解什么是“逃逸分析”:

[在 Java 中,对象通常被分配在堆内存中。堆内存由垃圾回收器(GC)管理,但频繁的堆分配和垃圾回收可能会导致性能开销。逃逸分析的目的是通过分析对象的使用范围,判断对象是否需要分配到堆内存中,或者是否可以通过其他方式优化内存分配。]

当一个对象发生逃逸,在栈内存上开始分配空间的时候。JVM还会进行第二次优化:通过标量替换来拆解对象

[标量替换是 Java 虚拟机(JVM)在逃逸分析基础上的一种优化技术。当逃逸分析确定一个对象不会逃逸出方法时,JVM 会将这个对象分解为其包含的标量成员变量,并将这些标量直接存储在栈帧或寄存器中,而不是在堆上创建对象。这样可以避免在堆上创建对象和垃圾回收的开销。]

这玩意听起来高端,说白了就是不存储对象,而是存储对象的各个字段。这些字段存储在栈上或寄存器中,访问速度更快,因为栈和寄存器的访问速度比堆快,提高了程序的执行效率。

而一个对象的逃逸不仅仅发生在方法层面,也有可能发生在线程层面。

比如下面这个代码中就发生了线程级别的逃逸:

代码语言:javascript
复制
public class Maintest {
    public void threadEscape() {
        Object object = new Object();
        new Thread(() -> {
            System.out.println(object); // 对象逃逸到其他线程
        }).start();
    }
}

而如果对象没有发生线程级别的逃逸,那么JVM就会开启同步消除技术,来消除没有必要的锁操作,例如下面的代码:

代码语言:javascript
复制
public class Maintest {
    public void threadEscape() {
        Object object = new Object();
        synchronized(object)
        {
            System.out.println(object);
        }
    }
}

在这段代码中,JVM会通过逃逸分析来判断这个对象在方法内被创建,且在方法执行期间,该对象不会被其他线程访问到,那么这个对象的同步操作就是多余的,JVM 就可以将其同步机制消除,从而减少不必要的性能开销。

经过上面的介绍,我们可以知道逃逸分析一共有以下三个优点:

那今天关于逃逸分析的文章就介绍到这里了。相信通过我的介绍,你已经大致了解了JVM中的逃逸分析。希望我的文章可以帮到你。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-01-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员牛肉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 比如下面这个代码中就发生了线程级别的逃逸:
  • 而如果对象没有发生线程级别的逃逸,那么JVM就会开启同步消除技术,来消除没有必要的锁操作,例如下面的代码:
  • 在这段代码中,JVM会通过逃逸分析来判断这个对象在方法内被创建,且在方法执行期间,该对象不会被其他线程访问到,那么这个对象的同步操作就是多余的,JVM 就可以将其同步机制消除,从而减少不必要的性能开销。
  • 经过上面的介绍,我们可以知道逃逸分析一共有以下三个优点:
  • 那今天关于逃逸分析的文章就介绍到这里了。相信通过我的介绍,你已经大致了解了JVM中的逃逸分析。希望我的文章可以帮到你。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档