接手“祖传无注释”代码后:记一次用大模型排查 Java 内存泄漏的完整工作流

📅 2026/7/2 17:24:41 👁️ 阅读次数 📝 编程学习
接手“祖传无注释”代码后:记一次用大模型排查 Java 内存泄漏的完整工作流

作为后端开发,接手遗留系统(俗称“祖传代码”)往往是一场噩梦,特别是遇到偶发性的内存泄漏(OOM)时。前不久,我们组接手了一个五年历史的 Java 业务模块,重构上线后没几天,就开始频繁触发老年代 GC 报警,随后节点陆续挂掉。由于代码中充斥着复杂的异步线程、生肉 SQL 以及未关闭的 I/O 流,通过常规的 MAT (Memory Analyzer Tool) 工具排查不仅耗时,而且很难快速关联到具体的业务代码行。

传统方式下,我们需要一点点 dump 内存、看引用链,再结合代码进行人工 Review。这次,我尝试转变思路,将 AI 大模型引入整个排查工作流,把繁杂的日志分析、逻辑推演和初步的重构方案交给机器。

为了减少来回切换工具的成本,并在不同模型间做交叉验证,我这次排查使用了一个能在同一界面切换 ChatGPT、Claude、Gemini、Grok 等大模型的聚合环境,方便把脱敏后的堆栈日志和可疑代码分别交给它们复跑,对比它们的分析结果。这篇文章就来复盘一下,我是如何拆解任务,一步步定位并解决这个隐蔽 Bug 的。


一、排错起点:从长日志的降噪与分析开始

遇到 OOM,第一步通常是看报错前后的堆栈和应用日志。遗留系统的日志往往非常冗杂,很多时候几千行日志里只有一行是真正有价值的报错上下文。

对于这类长文本日志的处理,我发现不同大模型的能力差异非常显著。我把最近 10 分钟内的 Tomcat 运行日志(约 12MB,去除了用户手机号等敏感信息)进行分段,让 AI 帮我寻找异常模式。

日志分析 Prompt 示例:

你是一个资深的 Java 性能诊断专家。 以下是一段脱敏后的应用运行日志(包含 GC 回收日志和多线程执行堆栈)。 请按以下步骤分析: 1. 提取出所有 OutOfMemoryError 或频繁 Full GC 发生前 3 分钟内出现的规律性异常现象; 2. 梳理这些异常涉及的业务类名、线程池名称或特定的定时任务; 3. 不要给我通用的“内存泄漏原因”科普,请只基于日志中实际出现的类名进行严谨推理。 [附带脱敏日志]

实测体验:

  • 当日志 Token 数较大时,部分模型容易出现“注意力丢失”,它会精准总结开头和结尾,但忽略了中间关键的报错点。
  • Gemini 在这方面的长上下文处理能力表现出色(比如 1.5 Pro),它精确扫描到了问题:在每天凌晨 2 点执行的数据同步任务日志中,有一个叫做DataSyncExecutor的线程池频繁抛出超时异常,且紧随其后的 GC 日志显示老年代回收效率极低。这就为后续的代码 Review 锁定了目标。

二、精细代码 Review:模型间的控制变量对比

在定位到具体的定时任务类(SyncTaskHandler)后,我将该类及其依赖的工具类共约 600 行代码提取出来,并去除了真实的表名和接口地址,开始寻找内存泄漏的根因。

在这个环节,我把同一套 Prompt 发给了几个不同的模型进行横向对比验证。排查复杂逻辑需要模型具备极强的代码阅读和上下文关联能力。

排查代码问题的 Prompt 示例:

以下是一段 Java 定时任务代码,主要实现从第三方 API 拉取海量数据并批量写入本地 MySQL。 目前系统在这段代码执行时存在严重的内存泄漏(堆内存不断上涨,Full GC 无法回收)。 请仔细 Review 代码: 1. 找出可能导致对象无法被回收的底层逻辑缺陷(如 I/O 流未关闭、大对象集合不断膨胀、ThreadLocal 未清理等); 2. 明确指出对应的行数和方法名; 3. 提供修复此缺陷的重构代码片段,要求符合 JDK 8 规范。 [附带脱敏代码]

输出结果的差异分析:

  1. 常规发现:ChatGPT 和 Grok 都迅速指出了代码中为了去重使用的一个全局HashSet没有任何清理机制,随着数据量增加必然导致 OOM。它们给出的方案是使用基于时间的缓存(如 Guava Cache)来替代。
  2. 深度挖掘:Claude 的表现则让我感到惊喜。它不仅指出了HashSet的问题,还敏锐地发现了更深层的隐患:代码中ThreadLocal变量在记录任务上下文后,没有在finally块中调用remove()。因为这是一个被复用的线程池,导致线程不仅没有销毁,其绑定的对象引用一直被持有,构成了经典的 ThreadLocal 内存泄漏。此外,它还指出了 HttpClient 请求未正确消费 Response Entity,长此以往会导致 HTTP 连接池耗尽。

最终证明,在应对复杂对象的生命周期追踪和底层并发机制理解上,某些模型确实表现得更加细腻,这帮我们避开了潜在的二次翻车。


三、代码重构与质量保障机制

找到病因后,下一步是对这段“意大利面条”代码进行重构,并补充单元测试。遗留代码最大的问题是职责不单一,一个方法里塞进了网络请求、数据清洗和数据库写入。

我让 AI 按照 SOLID 原则将原来的大方法拆分成三个独立的服务类,并要求为核心的清洗逻辑生成单元测试。

在生成测试用例时,早期我发现 AI 往往会生成大量“为了覆盖率而写”的无脑 Assert。因此,必须通过 Prompt 严格约束测试的有效性。

带约束的单测 Prompt:

基于以上重构好的 `DataCleanService` 类,请使用 JUnit 5 和 Mockito 编写完整的单元测试。 要求: 1. 必须覆盖输入数据包含 null 字段、空字符串以及极大值等边界情况; 2. 不能只写 assertNotNull,必须断言具体业务逻辑(如特定条件下的返回值、抛出自定义异常的类型); 3. 给出 Mock 对象的初始化过程,不要编造不存在的 Mockito 注解。

在这个阶段,只要你的 Prompt 足够具体,主流大模型的补全能力都很强,大约帮我节省了 60% 手敲单测 boilerplate 代码的时间,并且测试覆盖率直接拉到了 85% 以上。


四、安全边界与落地经验

虽然多模型协作极大地缩短了排错周期,但在真实的开发工作流中,我们必须建立清晰的边界感,否则很容易带来合规风险和新的线上故障。

1. 严格的代码与数据脱敏

这是绝不能妥协的红线。我们不能把包含真实 DB 密码、云服务 AK/SK、核心算法甚至真实客户数据的完整代码喂给大模型。在将代码粘贴出去之前,所有的脱敏都在本地 IDE 中通过正则或脚本提前完成。类名和变量名可以用 A、B、C 替换,只要保留逻辑的控制流和数据流结构,AI 依然能准确指出逻辑漏洞。

2. 警惕“看起来很完美的幻觉代码”

在其中一次 AI 给出的 HTTP 请求重构方案中,为了让代码显得简洁,它使用了一个 Apache HttpClient 中并不存在的便捷静态方法。这导致代码复制到本地后直接编译报错。所以,无论模型给出的代码多么优雅,绝对不能直接 Copy 上线,必须经过本地 IDE 语法校验、编译通过,并完整跑通回归测试。AI 提供的是“思路”,真正对线上质量负责的仍然是开发者。

3. 多模型交叉验证是防范翻车的有效手段

在长达一周的排错与重构周期里,我深刻体会到单一模型的局限性。有的模型适合吞咽海量报错日志,有的模型在寻找深层并发 Bug 上直觉惊人,有的则在写模板代码时速度最快。对于难以定位的诡异 Bug,把同样的问题同时抛给几个不同的模型,观察它们的共识和分歧,往往能触发我们意想不到的灵感。


五、结语

这次排查祖传代码 OOM 的经历,改变了我对 AI 辅助编程的传统认知。它不再仅仅是一个帮你写 Getter/Setter 的工具,而是变成了一个可以和你反复探讨并发设计、梳理遗留逻辑、Review 潜在漏洞的技术结对伙伴。

如果你也面临修复复杂旧系统的困境,建议不要一上来就把整个项目的文件打包丢给 AI。比较务实的做法是:先从一次具体的异常堆栈切入,抽象出一段几十行的独立方法,写好清晰并附带背景的 Prompt,放到支持多模型对比的环境中跑一跑,最后再用本地测试用例去严谨验证。

把庞大的技术债拆解成微小、可控、可验证的任务,才是当下开发者使用大模型破局最稳妥的路径。