PAYATHON 2026

NewRelic agent 9.1.0 是否导致 Tomcat 10.1 重新部署时内存泄漏?

数据小许

能否确定是 NewRelic 导致的内存泄漏?

目前只能确定,应用停止后,仍有涉及 NewRelic 的后台活动访问旧 Web 应用的类加载器。NewRelic 是重点排查对象,但这两段日志不足以确认 agent 9.1.0 存在已知缺陷,也不能单独证明旧应用会一直无法回收。

确认泄漏需要找到旧 WebappClassLoader 到 GC Roots 的强引用路径,并观察它是否在多次重新部署、垃圾回收后仍然存活和累积。解决方向取决于引用路径,可能涉及线程的上下文类加载器、后台任务、应用自身持有的引用或 agent 的内部对象。

两段日志分别说明了什么?

第一段日志显示,Tomcat 停止应用时发现 New Relic Token Expiration Handler 线程仍然存活。堆栈停在 LinkedBlockingQueue.take(),说明采集时线程正在等待任务,不能据此认定它在忙循环。

需要检查该线程是否通过 contextClassLoader、任务对象或其他字段持有旧应用的类加载器。线程名称可以提供排查线索,但不能证明具体的引用关系。

第二段日志显示,NewRelic 的 instrumentation 匹配逻辑尝试通过已停止的 WebappClassLoader 读取类资源,Tomcat 因此抛出 IllegalStateException。

DeflateInputStream.class 是被请求的资源,这里的异常发生在应用停止之后。不能据此认定缺少 Apache HttpClient 依赖,也不应先通过添加 JAR 来修复。

这两段日志还不能证明问题由同一条线程或同一条引用链造成,需要结合线程快照和堆转储判断。

先核对加载方式,再复现问题

先记录 Tomcat 的完整补丁版本、Java 版本、agent 的实际加载版本及部署方式。问题中提到的“最新版本”只是提问时的描述,不能据此推断当前版本状态。

重点核对:

  • JVM 启动参数中的 -javaagent 是否指向预期的 NewRelic agent,是否存在重复配置。
  • 应用的 WEB-INF/lib 和 Tomcat 共享库目录中,是否额外放入了 agent 实现 JAR。检查时要区分应用依赖 NewRelic API 和部署 agent 实现。
  • 问题是否只发生在同一个 JVM 内的重新部署过程中,完整重启后是否暂时消失。
  • 应用是否使用 NewRelic 异步 Token API。如果使用,检查 Token 是否按实际 API 要求结束生命周期。线程名中出现 Token,并不代表应用一定使用了该 API。

在测试环境中保持应用包、请求负载和配置一致,执行数次“部署、访问、停止、重新部署”。每轮记录线程数量、内存变化和异常出现时间,观察垃圾回收后的存活对象趋势。单看 JVM 占用内存上涨,无法确认泄漏。

采集线程快照和堆转储

可以使用目标 JVM 对应 JDK 中的 jcmd。将下面的 PID 替换为实际 Tomcat 进程号,并确保当前目录可写:

pid=12345

jcmd "$pid" VM.command_line
jcmd "$pid" Thread.print -l > threads-after-redeploy.txt
jcmd "$pid" GC.heap_dump heap-after-redeploy.hprof

建议在重新部署前和复现问题后分别采集,并使用不同文件名保存。堆转储可能暂停 JVM、占用较大磁盘空间,也可能包含请求内容或凭据等敏感数据,应选好采集时间和保存位置。

在线程快照中,查看 New Relic Token Expiration Handler 是否持续存在,以及异常中的 ClassesMatcher$1.run 是否反复出现。线程快照可以确认线程状态;具体持有哪些对象,仍需查看堆转储。

使用 Eclipse MAT 等工具检查堆转储时,可以按以下顺序分析:

  1. 找到属于旧 [prod] 应用的 WebappClassLoader 实例,区分当前部署和已停止的部署。
  2. 查看旧类加载器到 GC Roots 的引用路径,排除弱引用等非强引用路径。
  3. 检查引用是否经过线程的 contextClassLoader、ThreadLocal、队列任务、静态集合或 NewRelic 对象。这些方向仍需验证,日志尚未证实具体原因。
  4. 对比多轮部署后的结果,确认旧类加载器是否持续累积,以及对应应用对象是否仍被保留。

如果发现存活线程的 contextClassLoader 强引用旧类加载器,就找到了明确的保留路径;如果引用来自其他对象,应沿那条路径定位生命周期管理问题。

保留 NewRelic 时如何处理?

根据证据做版本对照

现有材料没有提供已确认的缺陷编号或修复版本,不能直接指定某个版本一定能解决问题。可以在测试环境中对照此前运行正常的版本,或核实发行说明后选择候选版本,再用相同的负载和部署步骤验证。

判断是否改善,要看旧类加载器能否回收、线程是否合理退出,以及异常是否停止。日志减少本身不能确认内存泄漏已经消失。

根据引用来源修复生命周期

如果旧类加载器由应用自己的线程池、定时任务、监听器或静态对象持有,应在应用停止时取消任务、关闭资源并释放相应引用。停止接收新请求并不意味着后台任务已经全部结束。

如果强引用路径明确落在 NewRelic 内部对象或线程上,应整理最小复现步骤、完整版本信息、agent 配置、线程快照及引用路径,交由 NewRelic 支持进一步确认。不要猜测某个配置项能够关闭问题,也不要在应用代码中强行停止 agent 的内部线程。

调整部署方式作为临时缓解

如果问题只在同一 JVM 内反复重新部署时出现,可以暂时采用重启 Tomcat 进程的部署流程,并在进程启动时继续启用 NewRelic。这样能清除旧 JVM 中的残留状态,但只是绕过触发条件,不能替代根因修复。

如需判断 agent 是否是必要触发条件,可以在测试环境短暂关闭 agent,进行一次对照实验。实验用于缩小排查范围,不要求生产环境长期停用监控。

如何确认处理有效?

修复后重复原来的部署和请求步骤,确认旧部署的类加载器不再持续累积,应用停止后不再反复访问已停止的类加载器,且垃圾回收后的堆占用没有随部署次数持续增长。

降低日志级别只能隐藏现象;强制调用 GC 也无法回收仍被强引用的旧应用。需要消除的是不再需要的引用或后台活动。

备注:内容仅供参考。