先讲答案:提交到线程池的异步任务不会被GC。
因为:
CompletableFuture
的任务被提交到线程池中,任务的执行就由线程池管理。
即使没有引用指向 CompletableFuture
对象,线程池中的任务仍然会继续执行,直到完成。CompletableFuture
对象的生命周期是解耦的。线程池会独立完成任务的执行,而不会因为 CompletableFuture
对象被垃圾回收而中断。那么,CompletableFuture<Void>对象会被GC掉吗?
先不急着给结论,Hold住这个问题,结合GC的原理考虑考虑。
下面来处理下我们关心的事:异步任务为什么不会被GC?
threadInStackSimple方法如下【可运行代码文件链接见文末】:
@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);
}
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如下 :
synchronized
块锁定的对象。ThreadLocal
变量小结一下:
Java 使用可达性分析算法来判断对象是否可以被回收。JVM 从一组称为 GC Roots 的对象开始,递归遍历所有可达的对象,并标记这些对象为存活。无法从 GC Roots 到达的对象被认为是垃圾,可以被回收。
常见的 GC Roots 包括:
Java 提供了四种引用类型,它们决定了对象在 GC 时的行为:
在讲哪些对象会被回收之前,先给threadInStackSimple方法中的变更归归类。
一共有3个变量:sseEmitter、completableFutureList、completableFuture
局部变量两个:
completableFutureList:List<CompletableFuture<Void>>
completableFuture:CompletableFuture<Void>
剩下的sseEmitter不确定。需要结合上游的代码,此处不展开
按照java的GC机制,threadInStackSimple方法执行完成后,局部变量
completableFutureList、completableFuture因为没有GCRoot引用,会被回收。
List<CompletableFuture<Void>> result = new ArrayList<>();
completableFutureList会被回收。显而易见,没有哪个GC Roots引用这个局部变量。
completableFuture呢?这也是局部变量!!!只是被GC的局部变量
completableFutureList引用。
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
两个接口:
一个触发threadInStackSimple方法的调用;
一个触发GC。
-Xlog:gc*:stdout
将 GC 日志输出到控制台。
调用接口:
GET http://localhost:90/stack/gc
先调用接口http://localhost:90/stack/gc,
再调用http://localhost:90/stack/gc/just,触发GC。
最后,执行Thread Dump,查看提提交到线程池中任务是否被GC。
可以看到,提交到线程池中的任务没有被GC。
小结一下
实际执行情况与我们分析的相同:
CompletableFuture的任务被提交到线程池中,该任务的执行就由线程池管理。在任务没有执行完成前,不会被GC。
示例中用到的代码:
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()
的原因主要与性能、不可预测性以及垃圾回收机制的设计有关。以下是具体原因:
System.gc()
通常会触发一次 Full GC,即对整个堆内存(包括年轻代和老年代)进行垃圾回收。Full GC 比年轻代 GC(Minor GC)更耗时,因为它需要处理更多的内存区域,并且可能涉及复杂的内存整理操作(如压缩)。System.gc()
会导致程序频繁暂停,严重影响应用的响应时间和吞吐量。System.gc()
只是一个建议,JVM 可以选择忽略它。虽然在大多数情况下,System.gc()
会触发一次 Full GC,但 JVM 的实现和配置可能会影响其行为。例如,在某些低延迟的垃圾回收器(如 ZGC 或 Shenandoah)中,System.gc()
的行为可能与预期不同。System.gc()
可能会干扰这些自动优化机制,导致垃圾回收器无法正常工作。-XX:MaxGCPauseMillis
、-XX:GCTimeRatio)。
虽然不建议频繁调用 System.gc()
,但在某些特殊场景下,显式触发 GC 可能是合理的: