无限链式调用的CompletableFuture是否会被垃圾回收?
首先你的无限链式实现本身不会因为thenCompose的链式引用导致旧CompletableFuture无法回收,真正存在内存风险的点是你代码里的全局eventFutureMap,以及未处理的线程安全问题。
- JDK的
CompletableFuture的依赖引用仅在前置任务未完成时存在:当你通过existingEvent.thenCompose()生成新的eventFuture后,一旦existingEvent执行完成、回调逻辑执行完毕,新的eventFuture会主动清除对existingEvent的引用,不会持有整条链式的所有历史节点。 - 旧的
CompletableFuture在完成、且被eventFutureMap移出引用后,没有任何强引用指向它,会被GC正常回收,哪怕你已经串了上万个链式节点也不会残留。
1. 线程安全隐患
你当前用的HashMap是非线程安全的,多线程并发调用publish()方法时,get和put操作存在竞态条件,轻则丢失链式更新导致事件顺序不保证,重则触发HashMap扩容死循环导致服务挂死,建议直接替换为ConcurrentHashMap。
2. 全局Map的冷key内存泄漏
你每次都会把最新的CompletableFuture存入eventFutureMap,如果某个key后续很长时间没有新事件进入,该key对应的最后一个CompletableFuture会被Map一直强持有,永远无法被回收,当冷key数量多了之后就会引发内存泄漏。
在生成eventFuture之后追加完成回调,判断当前Map中存储的是否还是当前这个CompletableFuture:如果是,说明该key暂时没有新事件进入,可以直接移除key释放引用,不会影响后续新事件的顺序保证。
优化后代码示例:
// 替换为线程安全的ConcurrentHashMap Map<Integer, CompletableFuture<EventResult>> eventFutureMap = new ConcurrentHashMap<>(); Publisher publisher; public CompletableFuture<EventResult> publish(Event event) { Integer key = event.getKey(); CompletableFuture<EventResult> existingEvent = eventFutureMap.get(key); CompletableFuture<EventResult> eventFuture; if (existingEvent != null) { eventFuture = existingEvent.thenCompose(meta -> publisher.publish(event)); } else { eventFuture = publisher.publish(event); } // 追加完成回调,无新事件时自动清理key eventFuture.whenComplete((res, throwable) -> { // 只有当前Map中存储的还是自己的时候才清理,避免误删新提交的任务 eventFutureMap.remove(key, eventFuture); }); eventFutureMap.put(key, eventFuture); return eventFuture; }
注意
ConcurrentHashMap的remove(key, value)是原子操作,不需要额外加锁,只有当key对应的值完全等于传入的value时才会删除,完美避免误删新提交的后续事件任务。
你可以通过添加虚引用监控,或者本地压测后dump内存分析,确认已经完成的旧CompletableFuture不会残留,只有未完成的任务和Map中暂存的最新任务会被持有。
内容的提问来源于stack exchange,提问作者James Hp

