第3章 OkHttp连接池泄漏路径:Dispatcher未关闭、RealCall引用链、ConnectionPool持有
3.1 现象识别
连接池泄漏的外在表现与线程泄漏高度关联,但在堆栈分析中呈现出不同的引用链特征。常见现场包括:
- OOM GC 压力:反复 Full GC,但堆中
okhttp3.ConnectionPool实例数量持续增长,且每个实例均关联多个RealConnection。 - 线程爆炸:
OkHttp Dispatcher的runningAsyncCalls数超过预期,且readyAsyncCalls或cancelledCalls队列中堆积大量从未出队的AsyncCall。 - 连接数居高不下:通过
adb shell dumpsys meminfo或Runtime.getRuntime().totalMemory()观察到OkHttp ConnectionPool内部的Deque<RealConnection>膨胀,即使应用已进入空闲状态。 - MAT 泄漏疑点:在 Eclipse MAT 或 IntelliJ Profiler 中,从
GC Root到ConnectionPool的路径中,Dispatcher始终是强引用链的起点之一。
RealConnection 数量远超 maxIdleConnections 默认值 5,优先检查 Dispatcher 是否被意外长期保留。
3.2 数据采集
收集泄漏路径所需的数据包括:
- 堆转储文件:在泄漏现场执行
jmap -dump:live,format=b,file=leak.hprof <PID>(Android 上使用adb shell am dumpheap <包名> /data/local/tmp/heap.hprof)。 - 线程快照:
jstack <PID>观察OkHttp Dispatcher线程状态。 - 存活对象统计:用 MAT 或
jhat分析okhttp3.internal.connection.RealConnection实例数,并查看其 GC Root 引用链。
# Android 堆转储命令示例
adb shell am dumpheap com.example.app /data/local/tmp/heap.hprof
adb pull /data/local/tmp/heap.hprof .
- Android Studio Profiler(直接观察连接池大小)
- Eclipse MAT 的 Leak Suspects 报告
- 命令行
jcmd <PID> GC.class_histogram 快速预览
3.3 原因定位
泄漏的根本路径是:Dispatcher 未正确关闭 → 队列中的 AsyncCall 持有 RealCall → RealCall 持有 Connection → Connection 被 ConnectionPool 持有 → 整个链路无法被 GC。下面从三个关键类分析每个环节。
3.3.1 Dispatcher 未关闭
当应用通过 OkHttpClient.Builder().dispatcher(dispatcher).build() 自定义 Dispatcher 时,若在 Activity/Fragment 销毁时未调用 dispatcher.executorService().shutdown(),则 ExecutorService 中的工作线程一直存活。即使 AsyncCall 已执行完毕,Dispatcher 的 readyAsyncCalls 和 cancelledCalls 队列仍可能因未及时清理而持有过时的 AsyncCall 引用。
// 典型泄漏代码:自定义 Dispatcher 后忘记关闭
Dispatcher dispatcher = new Dispatcher(new ThreadPoolExecutor(
0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>()));
OkHttpClient client = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
// ... 在 lifecycle 结束时未执行 dispatcher.executorService().shutdown();
3.3.2 RealCall 引用链
每个 AsyncCall 内部持有 RealCall 引用,而 RealCall 又持有 Transmitter,Transmitter 持有 ExchangeFinder,最终指向 RealConnection。当 Dispatcher 队列中的 AsyncCall 未被移除时,整条引用链被强根保留:
// 引用链示例(MAT 展示路径)
Dispatcher
-> runningAsyncCalls (ArrayDeque)
-> AsyncCall
-> RealCall
-> Transmitter
-> ExchangeFinder
-> RealConnection
-> ConnectionPool (connections)
3.3.3 ConnectionPool 持有
即使 RealConnection 已经空闲,ConnectionPool 的 connections 双端队列仍会按 keepAliveDuration(默认5分钟)保留最大 maxIdleConnections(默认5)个连接。但这是正常行为。真正的问题在于 被 Dispatcher 队列强引用的 RealCall 阻止了 RealConnection 进入空闲清理流程——因为连接仍在被“使用中”(虽然实际上任务已结束)。
| 环节 | 持有对象 | 泄漏影响 |
|---|---|---|
| Dispatcher | AsyncCall(通过 running/ready/cancelled 队列) | 线程无法退出,队列对象无法回收 |
| AsyncCall → RealCall | 强引用 | RealCall 及旗下 All 连接被保留 |
| RealCall → Connection | Transmitter → ExchangeFinder → RealConnection | 连接无法归还池或释放 |
| ConnectionPool | RealConnection(connections 队列) | 池中连接永远无法被驱逐 |
ConnectionPool 的 GC Root 路径。若最短路径经过 java.util.concurrent.ThreadPoolExecutor$Worker 或 OkHttp Dispatcher 的 runningAsyncCalls,则确认是 Dispatcher 未关闭 导致的泄漏;若路径来自 OkHttpClient 静态全局持有,则属于 长生命周期引用 泄漏。
3.4 修复动作
针对三个泄漏节点进行修复:
3.4.1 关闭 Dispatcher
在 Activity/Fragment 的 onDestroy() 或 Service 的 onDestroy() 中,明确关闭自定义 Dispatcher:
// 方案一:直接在生命周期关闭
@Override
protected void onDestroy() {
super.onDestroy();
if (dispatcher != null) {
dispatcher.executorService().shutdown(); // 不再接受新任务
// 可选择 awaitTermination 等待现有任务完成
}
}
若使用 OkHttpClient 默认 Dispatcher(通过 new OkHttpClient() 生成),则无需手动关闭——客户端本身会在 JVM 回收时清理。但若使用 OkHttpClient.Builder() 并显式设置 Dispatcher,则必须关闭。
3.4.2 清理队列中的悬空 AsyncCall
在关闭 ExecutorService 前,先清空 runningAsyncCalls、readyAsyncCalls 和 cancelledCalls,避免引用滞留:
// 方案二:完全重置 Dispatcher
dispatcher.cancelAll(); // 取消所有进行中的调用
dispatcher.executorService().shutdownNow(); // 中断工作线程
3.4.3 释放 ConnectionPool 中的空闲连接
虽然 ConnectionPool 有自动清理,但作为预防,可在关闭前主动驱逐空闲连接:
// 可选:强制清理空闲连接
connectionPool.evictAll(); // 清除所有空闲连接
OkHttpClient 声明为 Application 级别的单例,只创建一次,不进行 Dispatcher 自定义。若必须自定义 Dispatcher,请在 BaseApplication 的 onTerminate() 中统一关闭。
3.5 复测
修复完成后,进行以下验证:
- Heap 快照对比:在相同场景下(如连续切换 20 个 Activity + 网络请求)采集两次堆转储,对比
ConnectionPool和RealConnection实例数。修复后应稳定在maxIdleConnections(默认 5)附近。 - 线程数监控:通过
Thread.activeCount()或adb shell "ps -T <pid>"观察 OkHttp 工作线程数量,不再随请求次数线性增加。 - MAT 路径验证:为每个
ConnectionPool实例生成最短 GC Root 路径,确认不再经过Dispatcher的runningAsyncCalls。
# 快速验证:查看连接池大小(Android API 29+)
adb shell dumpsys activity provider com.android.shell:suspend | grep -i connectionpool
3.6 预防
| 预防措施 | 说明 | 应用场景 |
|---|---|---|
| 单例 OkHttpClient | 整个应用中只创建一次 OkHttpClient,复用 Dispatcher 和 ConnectionPool | 所有 Android 项目 |
| 避免自定义 Dispatcher | 除非有明确的隔离需求(如不同 baseUrl 使用不同线程池),否则使用默认 Dispatcher | 大多数普通项目 |
| 生命周期绑定 | 若必须自定义,将 Dispatcher 的生命周期绑定到 Application 而非 Activity | 大型组件化项目 |
| 弱引用回调 | 在 Callback 中使用 WeakReference 持有 Context 或 View,避免长生命周期的 Call 间接持有短生命周期对象 | Retrofit + OkHttp 混合使用 |
| 定期监控 | 在 CI 流程中加入堆转储分析脚本,对 ConnectionPool 数量设置阈值报警 | 线上质量监控 |
Retrofit 时,若 Service 接口方法返回 Call<T> 且未在生命周期结束时调用 call.cancel(),则该 Call 同样会进入 Dispatcher 队列导致泄漏。建议将 Call 绑定到 CompositeDisposable(RxJava 场景)或 Disposable(Coroutine 场景)统一取消。