深挖 CompletableFuture.waitingGet:JDK 1.8.221 的内存隐患与 1.8.441 的修复
深挖 CompletableFuture.waitingGet:JDK 1.8.221 的内存隐患与 1.8.441 的修复
核心问题:在 JDK 1.8.221 中,高并发调用
CompletableFuture.get()会导致Signaller节点在 completion stack 上残留,引发内存持续增长。本文从源码层面剖析成因,并给出版本对比与实践建议。
一、waitingGet 是什么
当 CompletableFuture 尚未完成时,调用 get() 会进入 waitingGet 方法:
// JDK 8 源码路径:java.util.concurrent.CompletableFuture
public T get() throws InterruptedException, ExecutionException {
Object r;
return reportGet((r = result) == null ? waitingGet(true) : r);
}
waitingGet 的核心流程:
private Object waitingGet(boolean interruptible) {
Signaller q = null;
boolean queued = false;
int spins = ...;
Object r;
while ((r = result) == null) {
if (spins > 0) {
// 自旋等待
} else if (q == null) {
q = new Signaller(interruptible, 0L, 0L); // ① 创建 Signaller
} else if (!queued) {
queued = tryPushStack(q); // ② 压入 completion stack
} else {
ForkJoinPool.managedBlock(q); // ③ 挂起等待
}
}
// Future 完成后的清理
if (q != null) {
q.thread = null; // ④ 清除线程引用
if (q.next != null)
cleanStack(); // ⑤ 有条件地清理链表
}
return r;
}
Signaller 是一个内部类,持有当前调用线程的强引用:
static final class Signaller extends Completion {
volatile Thread thread; // 强引用调用线程
Signaller(boolean interruptible, long nanos, long deadline) {
this.thread = Thread.currentThread();
}
}
二、JDK 1.8.221 的问题:清理条件不完备
2.1 cleanStack() 的调用条件
注意第 ⑤ 步:
if (q.next != null)
cleanStack();
只有当 q.next != null(即 Signaller 后面还有其他节点)时,才会执行 cleanStack()。
如果 q 是 completion stack 上的唯一节点,或者是栈顶节点而 next 为 null,则 cleanStack() 不会被调用。此时:
q.thread虽然被置 null(Thread 引用断开)- 但
q节点本身仍然挂在CompletableFuture的stack字段上 - 只要
CompletableFuture对象还被引用,q就不会被 GC
2.2 高并发下的叠加效应
在线程池中,多个线程同时 get() 同一个 Future 是常见场景:
线程 A → 创建 Signaller-A → 压入 stack(stack: A)
线程 B → 创建 Signaller-B → 压入 stack(stack: B → A)
线程 C → 创建 Signaller-C → 压入 stack(stack: C → B → A)
Future 完成,postComplete() 依次唤醒各线程
→ 每个 Signaller 的 tryFire() 被调用
→ thread 字段置 null
→ 但节点是否被从 stack 上摘除,取决于 cleanStack() 是否被正确触发
在特定竞态条件下,部分 Signaller 节点会残留在已完成的 CompletableFuture 的 stack 链上。
2.3 内存影响
| 影响 | 说明 |
|---|---|
| Signaller 节点泄漏 | 完成后的 Future 仍持有 Signaller 链表,节点无法被 GC |
| Old 区缓慢增长 | Signaller 对象从 Young 区晋升到 Old 区后长期驻留 |
| GC 压力上升 | 更多对象存活 → Minor GC 频率上升 → 偶发 Mixed GC |
| 堆内存碎片化 | 大量小对象长期驻留 Old 区,加剧碎片 |
三、通过 Heap Dump 验证
在 JDK 1.8.221 的服务中,如果存在这个问题,Heap Dump 中可以看到:
Class Name | Instances | Shallow Heap
-----------------------------------------------------------------
java.util.concurrent.CompletableFuture$Signaller | 42,318 | 1,352,576
java.util.concurrent.CompletableFuture | 38,104 | 1,219,328
大量 Signaller 实例,且数量与 CompletableFuture 实例数不成比例(正常情况下应接近 1:1),是泄漏的典型特征。
使用 MAT(Eclipse Memory Analyzer)查看 Signaller 的 Retained Heap,会发现引用链:
CompletableFuture.stack
└── Signaller (next → Signaller → ...)
└── thread = null ← thread 已被清除,但节点本身仍可达
四、JDK 1.8.441 的修复
JDK 8 在后续 patch 版本中对 waitingGet 的清理逻辑进行了加固,核心改动方向:
4.1 cleanStack() 调用无条件化
修复后,退出 waitingGet 时无论 q.next 是否为 null,都会尝试清理:
// 修复后(行为示意)
if (q != null) {
q.thread = null;
cleanStack(); // 不再有 if (q.next != null) 的条件限制
}
4.2 postComplete() 遍历时主动摘除节点
修复后,postComplete() 在遍历 completion stack 并触发各节点时,会主动将已处理的节点从链表中摘除,而不是依赖调用方的事后清理。
4.3 修复效果
| 指标 | JDK 1.8.221 | JDK 1.8.441 |
|---|---|---|
已完成 Future 的 stack 字段 |
可能残留 Signaller 链 | 完成后 stack 清空 |
| Signaller 实例数(稳定态) | 持续增长 | 与活跃 Future 数量匹配 |
| Old 区增长趋势 | 缓慢但持续上升 | 平稳,随 GC 正常回落 |
五、缓解方案(无法立即升级时)
5.1 避免同步 get(),改用回调链
根本上减少 waitingGet 被调用的次数:
// ❌ 触发 waitingGet,有泄漏风险
String result = future.get();
// ✅ 全程异步,不进入 waitingGet
future.thenAccept(result -> process(result));
// ✅ 组合多个 Future 时用 allOf + join 替代逐个 get
CompletableFuture.allOf(f1, f2, f3)
.thenRun(() -> {
f1.join();
f2.join();
});
5.2 使用超时版 get()
// 带超时的 get() 走不同的代码路径,清理逻辑更完整
String result = future.get(3, TimeUnit.SECONDS);
5.3 定期监控堆内 Signaller 数量
通过 JMX 或 Actuator 定期抓取,建立告警基线:
# 查看 Signaller 实例数(需开启 JMX)
jmap -histo <pid> | grep Signaller
若 Signaller 数量持续增长而不回落,说明泄漏正在发生。
六、升级检查清单
升级到 JDK 1.8.441 后,建议同步完成以下检查:
# 1. 确认版本
java -version
# 输出应包含:build 1.8.0_441-xxx
# 2. 保留必要的 GC 参数(和版本无关,建议保留)
-XX:+UseG1GC
-XX:+ParallelRefProcEnabled # 并行引用处理,防御性配置
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 3. 升级后观察 Old 区趋势(连续 24h)
# 预期:Old 区不再持续单调增长
七、总结
| 问题 | 成因 | 影响 | 解决方案 |
|---|---|---|---|
| Signaller 节点残留 | cleanStack() 调用条件不完备 |
Old 区缓慢增长,GC 压力上升 | 升级 JDK 1.8.441 |
| 高并发下加剧 | 每次 get() 都创建 Signaller |
节点数随 QPS 线性积累 | 改用异步回调链 |
| 问题不易察觉 | 内存增长缓慢,无明显 OOM | 长期运行后才暴露 | 监控 Signaller 实例数 |
CompletableFuture 是 Java 并发编程的核心工具,但其内部实现的细节缺陷在高并发、长时间运行的服务中会被逐渐放大。升级至 JDK 1.8.441 是最直接有效的修复手段,结合业务侧减少同步 get() 调用,可以从根本上消除这一内存隐患。
说明:本文分析基于 JDK 8 源码中
waitingGet的清理逻辑,以及多个 patch 版本间的行为变化对比。如需定位线上问题,建议结合jmap -histo监控 Signaller 实例数作为判断依据。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)