接手“祖传无注释”代码后:记一次用大模型排查 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 规范。 [附带脱敏代码]输出结果的差异分析常规发现ChatGPT 和 Grok 都迅速指出了代码中为了去重使用的一个全局HashSet没有任何清理机制随着数据量增加必然导致 OOM。它们给出的方案是使用基于时间的缓存如 Guava Cache来替代。深度挖掘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放到支持多模型对比的环境中跑一跑最后再用本地测试用例去严谨验证。把庞大的技术债拆解成微小、可控、可验证的任务才是当下开发者使用大模型破局最稳妥的路径。