泄漏的基本概念:周期性 Work 未清理、资源泄漏类型、泄漏的典型后果

现象识别

周期性 Work 未清理泄漏在运行态表现为:

  • 内存持续增长(Heap / RSS 曲线呈阶梯状上升,触发 Full GC 后不回落)
  • 线程或连接池耗尽(新增 Work 无法提交,超时或阻塞)
  • 应用响应劣化(单次 Work 执行时间正常,但整体吞吐下降)
  • 监控告警(CPU 使用率与 GC 频率同步升高,OOM 风险)
🔍 现象口诀:内存只升不降,Work 只增不减,GC 越跑越慢。

数据采集

明确采集点及命令,用于后续分析:

采集对象工具/命令关键指标
JVM Heap 分布jmap -histo:live <pid>常驻对象计数、Work 相关类的实例数
线程堆栈jstack <pid>Work 线程是否卡在等待/阻塞态
GC 日志-Xlog:gc* (JDK 11+)Full GC 后老年代仍不降
Work 队列状态JMX / 自定义 MBeanqueueSize、completedCount、rejectedCount
💡 建议在压测阶段每 30s 采集一次快照,保留泄漏爆发前的基准。

原因定位

使用故障树逐步定位根因:

资源泄漏故障树(周期性 Work 未清理) 🍂 内存持续增长 / OOM Work 对象未释放(引用残留) Work 完成后未 remove 周期触发仍持有旧 Work 回调未调清理接口 schedule 未覆盖清理逻辑 📌 判断规则: • 若 Work 实例数随周期线性增长 → 检查注册/注销配对 • 若 GC 后老年代仍不降 → 存在本堆引用链(通常是定时器线程或集合)

修复动作

修复的核心是保证 Work 的生命周期闭环:

  1. 强制清理:在 Work 完成回调中显式调用 remove() 或 cancel()。
  2. 弱引用防护:对关键资源使用 WeakReference + ReferenceQueue。
  3. 超时兜底:在 Work 执行超过预期时间后自动释放关联资源。
// 修复示例:周期性 Work 完成后的清理
public class SafeWork implements Runnable {
    private final WorkQueue queue;
    private final WorkItem item;
    
    @Override
    public void run() {
        try {
            // 业务逻辑
        } finally {
            queue.remove(item);  // 确保从队列清除
            item.release();      // 释放文件句柄/连接等
        }
    }
}

复测

复测手段:

  • 稳定压测:运行相同负载 2 小时,Heap 曲线应持平或微波动。
  • 对象计数审计:jmap -histo:live 中 Work 相关类实例数应稳定在预期值内。
  • GC 日志验证:Full GC 后老年代使用率应回归基线。
⚠️ 复测时需确认修复后队列中出现新 Work 时仍能正常清理,避免因过度清理导致重复提交。

预防

将以下规则纳入代码规范和 CI 流程:

防线措施
静态检查Sonar 规则:ThreadPoolExecutor 提交的 Runnable 必须在 finally 中释放资源。
动态追踪为关键 Work 注册 StateListener,定期上报存活 Work 数量。
架构约束使用 ScheduledExecutorService 配合 Future 取消,或采用 ScheduledFuture.addListener 模式。
混沌工程在预发环境注入周期性 Work 模拟,自动检测对象泄漏。

额外建议:对资源泄漏类型建立分层清单,便于培训与复盘。常见的泄漏类型包括:

  • 内存泄漏(对象引用链不断开)
  • 句柄泄漏(文件描述符 / Socket / 数据库连接未归还)
  • 线程泄漏(线程被创建后未销毁)
  • 缓存泄漏(TTL 或 LRU 策略失效,导致缓存无限膨胀)

周期性 Work 未清理通常同时引发 内存泄漏 和 句柄泄漏,实践中需联合排查。