前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >CompletableFuture<Void>对象被GC了,引用的异步任务会被GC吗?

CompletableFuture<Void>对象被GC了,引用的异步任务会被GC吗?

作者头像
烟雨平生
发布2025-03-07 17:51:02
发布2025-03-07 17:51:02
6400
代码可运行
举报
文章被收录于专栏:数字化之路数字化之路
运行总次数:0
代码可运行

先讲答案:提交到线程池的异步任务不会被GC。

因为:

  • 线程池的独立性:一旦 CompletableFuture 的任务被提交到线程池中,任务的执行就由线程池管理。 即使没有引用指向 CompletableFuture 对象,线程池中的任务仍然会继续执行,直到完成。
  • 任务的生命周期:任务的生命周期与 CompletableFuture 对象的生命周期是解耦的。线程池会独立完成任务的执行,而不会因为 CompletableFuture 对象被垃圾回收而中断。

那么,CompletableFuture<Void>对象会被GC掉吗?

先不急着给结论,Hold住这个问题,结合GC的原理考虑考虑。

下面来处理下我们关心的事:异步任务为什么不会被GC?

threadInStackSimple执行后,会GC哪些对象

代码语言:javascript
代码运行次数:0
复制
threadInStackSimple方法如下【可运行代码文件链接见文末】:
代码语言:javascript
代码运行次数:0
复制
@Override
public void threadInStackSimple(SseEmitter sseEmitter) {
    List<CompletableFuture<Void>> completableFutureList = new ArrayList<>();
    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
        sendMsg(sseEmitter, "Send the first message");
        try {
            Thread.sleep(60_000);
        } catch (InterruptedException e) {
            log.info("报错了 {} ", e.getMessage(), e);
        }
        sendMsg(sseEmitter, "Send the last message");
        sseEmitter.complete();
    }, multiSSEExecutor).exceptionally(ex -> {
        log.info("报错了 {} ", ex.getMessage(), ex);
        sseEmitter.completeWithError(ex);
        return null;
    });
    completableFutureList.add(completableFuture);
}
代码语言:javascript
代码运行次数:0
复制
private static void sendMsg(SseEmitter emitter, String msg) {
    try {
        emitter.send(msg);
        log.info("发送成功一条 {} ", msg);
    } catch (IOException e) {
        log.info("报错了 {} ", e.getMessage(), e);
        throw new RuntimeException(e);
    }
}

在展开之前,先回忆下哪些对象会被GC掉?

无法从 GC Roots 到达的对象被认为是垃圾,可以被回收。

常见的GC Roots如下 :

  1. 虚拟机栈(Java 栈)中的局部变量
    • 方法调用时,栈帧中存储的局部变量和参数。
    • 如果这些变量引用了对象,这些对象就是 GC Roots。
  2. 方法区(元空间)中的静态变量
    • 类的静态字段引用的对象。
    • 静态变量的生命周期与类的加载和卸载相关。
  3. 方法区中的常量引用
    • 字面量常量(如字符串常量池中的对象)。
    • 这些常量在方法区中被引用,不会被垃圾回收。
  4. 本地方法栈中的 JNI 引用
    • Java Native Interface(JNI)中本地方法引用的 Java 对象。
    • 本地方法中通过 JNI 创建的对 Java 对象的引用。
  5. 活动线程
    • JVM 中正在运行的线程本身。
    • 线程的栈帧中可能包含对对象的引用。
  6. 正在同步的监视器(锁)
    • synchronized 块锁定的对象。
    • 这些对象在同步代码块执行期间不会被回收。
  7. Java 堆中的引用类型字段
    • 对象的实例字段引用的其他对象。
    • 如果对象本身是可达的,那么它的字段引用的对象也可能成为 GC Roots。
  8. JVM 内部的引用
    • Java 堆外的直接内存分配。
    • 特殊的系统类加载器引用的对象。
    • JVM 内部可能持有某些对象的引用,例如:
  9. JNI 本地引用
    • 通过 JNI 创建的全局引用和局部引用。
  10. 可达的 ThreadLocal 变量
    • 线程局部变量中引用的对象。

小结一下:

Java 使用可达性分析算法来判断对象是否可以被回收。JVM 从一组称为 GC Roots 的对象开始,递归遍历所有可达的对象,并标记这些对象为存活。无法从 GC Roots 到达的对象被认为是垃圾,可以被回收。

常见的 GC Roots 包括:

  • 虚拟机栈(栈帧中的局部变量)中引用的对象。
  • 方法区中静态变量引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。
2. 引用类型

Java 提供了四种引用类型,它们决定了对象在 GC 时的行为:

  • 强引用:只要对象有强引用指向它,就不会被回收。
  • 软引用:在内存不足时,软引用指向的对象会被回收。
  • 弱引用:在下一次 GC 时,弱引用指向的对象会被回收。
  • 虚引用:虚引用主要用于监控对象的回收,不会影响对象的生命周期。

在讲哪些对象会被回收之前,先给threadInStackSimple方法中的变更归归类。

一共有3个变量:sseEmitter、completableFutureList、completableFuture

局部变量两个:

completableFutureList:List<CompletableFuture<Void>>

completableFuture:CompletableFuture<Void>

剩下的sseEmitter不确定。需要结合上游的代码,此处不展开

CompletableFuture<Void>会被回收吗?

按照java的GC机制,threadInStackSimple方法执行完成后,局部变量

completableFutureList、completableFuture因为没有GCRoot引用,会被回收。

代码语言:javascript
代码运行次数:0
复制
List<CompletableFuture<Void>> result = new ArrayList<>();

completableFutureList会被回收。显而易见,没有哪个GC Roots引用这个局部变量。

completableFuture呢?这也是局部变量!!!只是被GC的局部变量

completableFutureList引用。

代码语言:javascript
代码运行次数:0
复制
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {

先讲答案:completableFuture不会被回收。

因为被线程池中的任务强引用。

原因在CompletableFuture.runAsync方法中:

java.util.concurrent.CompletableFuture#runAsync(java.lang.Runnable, java.util.concurrent.Executor)

java.util.concurrent.CompletableFuture#asyncRunStage

run下,看看实际情况

两个接口:

一个触发threadInStackSimple方法的调用;

一个触发GC。

代码语言:javascript
代码运行次数:0
复制
-Xlog:gc*:stdout

将 GC 日志输出到控制台。

调用接口:

代码语言:javascript
代码运行次数:0
复制
GET http://localhost:90/stack/gc

先调用接口http://localhost:90/stack/gc,

再调用http://localhost:90/stack/gc/just,触发GC。

最后,执行Thread Dump,查看提提交到线程池中任务是否被GC。

可以看到,提交到线程池中的任务没有被GC。

小结一下

实际执行情况与我们分析的相同:

CompletableFuture的任务被提交到线程池中,该任务的执行就由线程池管理。在任务没有执行完成前,不会被GC。

REFERENCE

示例中用到的代码:

https://gitee.com/baidumap/sse-chat/blob/main/src/main/java/cn/aias/ssechat/business/stream/SSEServiceImpl.java

Thread Dump,翻译为线程快照或javacore文件,是一个文本文件,记录了当前虚拟机内每一条线程正在执行的执行情况,包括线程的状态、堆栈信息等。它可以帮助我们分析和诊断多线程程序中的问题,可以帮助我们定位线程出现长时间停顿的原因。 唐成,公众号:的数字化之路全网最全的生成dump文件方法都在这了,肯定有你不知道的

IDEA中从0到1学会使用Thread Dump 唐成,公众号:的数字化之路没有日志,有什么办法能知道“这个任务跑到哪了?”

要使用线程池。 创建线程是耗时耗资源的操作,一般都会使用线程池。看上面的数据,同一个时间点,创建的线程数和销毁的线程数在同一个数据量级,很可能是new Thread方式创建的线程且执行耗时较短的任务,然后马上被GC回收引发。不像是使用了线程池。 唐成,公众号:的数字化之路为什么说SpringSession不适合用户量大的场景,你看看这个默认配置!!

FullGC会STW。 如果pod重启时,识别到FullGC耗时过长,则优先考虑增加内存来解决。 出现异常时,要把jvm堆内的数据dump出来。在没有找到异常原因时,要把dump出来的堆数据都查看一下,因为dump时,有的pod中的jvm可能刚启动不久,异常操作还没有被触发。 唐成,公众号:生活点亮技术FullGC没及时处理,差点造成P0事故

关于FullGC,关于示例中的System.gc():

在 Java 中,不建议频繁调用 System.gc() 的原因主要与性能、不可预测性以及垃圾回收机制的设计有关。以下是具体原因:

1. 性能开销

  • 触发 Full GCSystem.gc() 通常会触发一次 Full GC,即对整个堆内存(包括年轻代和老年代)进行垃圾回收。Full GC 比年轻代 GC(Minor GC)更耗时,因为它需要处理更多的内存区域,并且可能涉及复杂的内存整理操作(如压缩)。
  • 阻塞线程:Full GC 是一个阻塞操作,会暂停所有用户线程(Stop-the-World),直到垃圾回收完成。频繁调用 System.gc() 会导致程序频繁暂停,严重影响应用的响应时间和吞吐量。
  • 增加 CPU 负载:垃圾回收是一个资源密集型操作,频繁触发 GC 会增加 CPU 的负载,可能导致系统性能下降。

2. 不可预测性

  • 无法保证执行System.gc() 只是一个建议,JVM 可以选择忽略它。虽然在大多数情况下,System.gc() 会触发一次 Full GC,但 JVM 的实现和配置可能会影响其行为。例如,在某些低延迟的垃圾回收器(如 ZGC 或 Shenandoah)中,System.gc() 的行为可能与预期不同。
  • 与垃圾回收器冲突:现代 JVM 的垃圾回收器(如 G1、ZGC)通常会根据内存使用情况和应用性能动态调整垃圾回收的时机和策略。频繁调用 System.gc() 可能会干扰这些自动优化机制,导致垃圾回收器无法正常工作。

3. 导致内存碎片化

  • 频繁压缩内存:某些垃圾回收器(如 CMS 或 Serial GC)在 Full GC 时会进行内存压缩,以整理堆空间。频繁触发 Full GC 可能会导致内存碎片化,降低内存分配的效率。
  • 影响内存分配策略:频繁的 GC 可能会改变 JVM 的内存分配策略,导致年轻代和老年代的内存分配比例失衡,进一步影响性能。

4. 更好的替代方案

  • 让 JVM 自动管理:现代 JVM 的垃圾回收器已经非常智能,能够根据应用的运行时行为自动调整垃圾回收的频率和策略。通常情况下,开发者不需要手动干预垃圾回收。
  • 优化内存使用:如果频繁触发 GC 是因为内存不足或性能问题,应该从代码层面优化内存使用,例如减少不必要的对象创建、使用对象池、合理配置堆大小等。
  • 监控和调整垃圾回收器参数:如果需要优化垃圾回收行为,可以通过监控工具(如 JVisualVM、JConsole)分析 GC 日志,然后调整垃圾回收器的参数(如 -XX:MaxGCPauseMillis-XX:GCTimeRatio)。

5. 特殊场景的例外

虽然不建议频繁调用 System.gc(),但在某些特殊场景下,显式触发 GC 可能是合理的:

  • 内存泄漏排查:在调试阶段,显式触发 GC 可以帮助快速定位内存泄漏问题。
  • 资源清理:在某些资源密集型操作完成后,显式触发 GC 可以帮助释放内存,避免内存不足。
  • 测试环境:在测试环境中,显式触发 GC 可以模拟实际运行时的内存回收行为。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-03-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • threadInStackSimple执行后,会GC哪些对象
    • 2. 引用类型
  • CompletableFuture<Void>会被回收吗?
  • run下,看看实际情况
  • REFERENCE
    • 1. 性能开销
    • 2. 不可预测性
    • 3. 导致内存碎片化
    • 4. 更好的替代方案
    • 5. 特殊场景的例外
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档