要在生产环境中定位 Java 内存泄漏,你可以分析运行中 JVM 的 GC 对象引用链,找出哪些 GC Root 仍持有本应被释放的对象——全程无需暂停应用、无需导出堆转储、也无需重启进程。本文将通过一个真实的订单系统案例,展示如何用 OpenResty XRay 做到这一点。

我们采用“从面到点”的策略,分三步推进:

  • 定位泄漏:使用 GC 对象引用分析,找到非预期内存驻留的根源。
  • 排查抖动:使用 GC 对象分配次数分析,发现高频创建对象的代码路径。
  • 优化体积:使用 GC 对象分配大小分析,定位大对象的创建源头。

为什么多次 Full GC 后堆内存仍未回落:一次生产环境的 Java 内存泄漏

在一次客户的订单系统的性能回归测试中,我们观察到一个典型的内存异常现象:应用的堆内存曲线呈持续上升趋势,即便在多次 Full GC 之后,堆空间占用仍未见明显回落

根据工程经验,这种现象通常指向两个潜在问题:

  1. 存在内存泄漏 (Memory Leak),即部分对象在失去引用后本应被回收,但由于非预期的 GC Root 引用链存在,导致它们常驻内存。
  2. 在特定的业务路径上存在高频的对象创建“抖动” (Memory Churn),即大量短暂对象的创建和销毁导致 Young GC 压力过大,进而频繁触发 Full GC。

传统的堆转储 (Heap Dump) 分析方法,如使用 jmap 导出 HPROF 文件再通过 MAT 等工具离线分析,虽然功能强大,但在生产环境下面临挑战。导出数 GB 大小的堆快照本身会引发长时间的 STW (Stop-the-World),对线上服务是不可接受的。

为了在不中断服务、不引入明显性能开销的前提下进行“在线”诊断,我们采用 OpenResty XRay 对运行中的 JVM 进行分析。

如何在不导出堆转储的情况下定位生产环境的 Java 内存泄漏

我们采用了“从面到点”的诊断策略,首先确认是否存在泄漏,然后排查分配频率与大小问题。

第一步:通过 GC Root 引用分析定位泄漏

首要任务是确认是否存在内存泄漏,并找到泄漏源。我们使用了 OpenResty XRayGC 对象引用关系分析器

启动分析后,火焰图报告清晰地将内存中的对象分为两类:

  • Dead GC objects:已死亡、等待回收的对象(可忽略)。
  • 被 GC root 引用的对象:存活的对象(排查重点)。

我们重点排查后者。沿着 GC root 引用链逐层展开,分析工具清晰地指向了内存占用的集中点:OrderProcessingService$OrderService 实例。

引用路径图:OrderProcessingService OrderService 实例持有未释放的泄漏对象

进一步深入,发现内存主要被该实例的两个字段持有:

  • notificationQueue (一个并发队列)
  • paymentRecords (一个支付记录数组)

通过分析引用路径我们确认,这些集合中的对象本应在业务流程结束后被移除,但由于逻辑缺陷,导致它们的生命周期超出了预期,证实了内存泄漏点的存在。

第二步:用 GC 分配次数分析检测内存抖动

定位泄漏点后,第二个问题是:是否存在不必要的内存“抖动”?高频的内存分配会加剧 GC 压力,即使没有泄漏,也会拖累系统吞吐量。

通过 OpenResty XRayGC 对象分配次数分析器,我们得到了按代码路径分类的对象创建频率火焰图。

报告显示,createOrder 方法 (@OrderProcessingService.java:118) 的对象创建次数,也就是火焰图中的宽度,要显著高于其他业务路径。

高频的对象创建,尤其是进入老年代,会导致 Young GC 频繁触发,并且可能加速老年代的填充,这会增加 STW 时间,进而拖慢 P99 响应延迟。

第三步:用 GC 分配大小分析揪出大对象

最后,我们检查了“大对象”问题,即是否存在某些路径在频繁创建体积庞大的对象。

我们切换到 GC 对象分配大小分析器。GC 对象分配火焰图按照采样区间内创建的 GC 对象总大小进行统计。创建的对象总大小越大,在图上的占比越高。

分析报告指出,generateOrderId 方法 (@OrderProcessingService.java:155) 及其下游调用,虽然在次数上不占优,但在内存体积占用上却非常突出,主要原因是频繁创建了体积较大的字符串对象。

经评估,该 ID 生成逻辑有很高的重复率,适合引入缓存。通过对字符串生成逻辑进行缓存化改造,我们显著降低了该路径的堆占用峰值。

成效:更低的 GC 开销与更稳定的 P99 延迟

借助 OpenResty XRay 的三个分析器,我们在短时间内完成了从现象到根因的完整闭环定位。

  • 诊断效率提升:过去依赖传统 heap dump 工具,需要反复比对与手动追踪引用链;而使用 XRay 的火焰图后,仅需几分钟即可直观看到问题集中区域,大幅缩短了排查路径。
  • 系统性能改善:修复问题后,GC 时间占比明显下降,应用在相同负载下的响应更加平稳,整体延迟曲线趋于稳定。

更重要的是,这次实践带来了方法论上的变化。OpenResty XRay 的可视化分析让团队成员能直接理解对象在内存中的生命周期与引用关系。调优讨论从“凭经验猜测”转向“基于证据决策”,让性能优化变得更具确定性。

如何定位 Java 内存泄漏:一套可复用的三步法

上面的案例可以抽象成一套可复用的方法。无论你用什么工具,定位 Java 内存泄漏最有效的方式,都是按“从驻留到分配”的顺序依次回答三个问题:

  1. 哪些对象被持有,又是被谁持有的? 沿着 GC 对象引用链回溯到它们的 GC Root。泄漏表现为本应被释放、却仍通过某条非预期引用保持可达的对象——在本案例中,就是 notificationQueuepaymentRecords 这两个集合,其中的条目本应在业务流程结束后被移除,却没有。
  2. 哪些代码路径分配最频繁? 高频对象创建(内存抖动)即使没有泄漏,也会抬高 Young GC 压力并可能频繁触发 Full GC。分配次数分析能暴露这些热点路径——本案例中是 createOrder 方法。
  3. 哪些代码路径分配的内存最多? 有些路径创建的对象数量不多,但单个体积很大。分配大小分析能把它们揪出来——在本案例中,是 generateOrderId 中创建的大字符串对象。

从驻留(第一步)到分配频率与大小(第二、三步)逐层推进,能把排查范围从“内存是否在泄漏?”收窄到“到底是哪段代码在负责”。

为什么用 OpenResty XRay 在运行中的 JVM 上定位 Java 内存泄漏?

传统的 Java 内存分析严重依赖 Heap Dump。这种“离线尸检”式的分析方式,存在其固有的局限性。

传统 Heap Dump 工具核心局限
需暂停或重启进程1. 导出堆快照(jmap)通常会触发 STW,导致生产服务长时间停顿。
分析成本高2. 堆文件体积庞大(动辄几 GB),传输与加载耗时。
数据时效性差3. 只能分析当时的快照,无法观察系统实时变化。

OpenResty XRay 提供了另一种思路:非侵入式的“在线分析”。它在设计上规避了传统工具的痛点。

  1. 生产环境安全 OpenResty XRay 无需重启 Java 进程,也不必修改代码或替换 JDK。它通过安全的 attach 机制直接连接正在运行的 JVM,对业务透明。

  2. 极低开销与非侵入性 这是 OpenResty XRay 的核心技术优势。它不依赖 JVM 安全点 (Safepoint) 机制进行采样。这意味着它可以在不触发任何 STW (Stop-the-World) 的前提下,对 JVM 的内存、CPU、I/O 等进行分析。这使其成为少数真正敢在实时生产流量下执行深度诊断的工具。

  3. 直观的可视化分析 它提供的 GC 对象引用火焰图和内存引用路径图,能直观展示“谁引用了谁”、“内存去哪了”,极大降低了内存问题的分析门槛。

  4. 多维度的性能画像

OpenResty XRay 不仅限于内存。在本次案例中,我们虽然聚焦于 GC,但也可以随时切换到 CPU 火焰图、I/O 分析等,结合上下文构建一个完整的系统性能画像。

这种设计让 OpenResty XRay 成为少数真正可在 生产环境实时分析 Java 内存问题 的工具之一。在上面的订单系统案例中,它帮助团队实现了“实时诊断 + 快速验证”的闭环,避免了停机分析的风险。

关键要点:无需重启、无需停机地定位 Java 内存泄漏

在此次订单系统的内存问题排查中,OpenResty XRay 帮助我们构建了一个系统化的诊断流程:从“是否泄漏”到“频率问题”再到“对象体积”;从“引用关系”到“分配次数”再到“分配大小”层层递进,缩小排查范围。

这种基于实时采样与可视化的分析范式,能有效替代传统的 heap dump + 离线分析路径。它不仅提升了诊断效率,更重要的是,它为高并发、高可用的 Java 服务在生产环境中的性能调优,提供了一种更安全、更高效的解题思路。

常见问题

能不能不用堆转储就定位 Java 内存泄漏?

可以。不必用 jmap 导出 HPROF 堆快照再离线分析,OpenResty XRay 通过安全的 attach 机制连接运行中的 JVM,实时分析 GC 对象引用链。这就避免了传统堆转储工具动辄数 GB 的堆文件,以及“离线静态快照”的分析模式。

在生产环境分析 Java 内存会导致 Stop-the-World 停顿吗?

jmap 导出堆快照通常会触发 Stop-the-World (STW),意味着生产服务可能长时间停顿。OpenResty XRay 不依赖 JVM 安全点 (Safepoint) 机制采样,因此可以在不触发 STW 的前提下分析 JVM 内存——可用于实时生产流量下。

内存泄漏和内存抖动怎么区分?

内存泄漏是指本应被回收的对象,因非预期的 GC Root 引用链而持续存活;内存抖动 (Memory Churn) 则是短生命周期对象的高频创建与销毁,会抬高 Young GC 压力并可能频繁触发 Full GC。在本案例中,引用关系分析器确认了泄漏,而分配分析器暴露了 createOrder 路径上的抖动。

用 OpenResty XRay 诊断 Java 内存问题的三个步骤是什么?

案例采用“从面到点”的路径,用到三个分析器:(1)GC 对象引用关系分析器,通过 GC Root 引用链定位泄漏;(2)GC 对象分配次数分析器,发现高频对象创建(内存抖动);(3)GC 对象分配大小分析器,定位创建大对象的代码路径。

为什么不直接用 jmap 和 MAT?

本文指出传统堆转储工具有三个核心局限:用 jmap 导出快照通常会触发 Stop-the-World 并造成停机;堆文件动辄数 GB,传输与加载耗时;而且只能分析静态快照,无法观察系统的实时变化。

关于 OpenResty XRay

OpenResty XRay 是一款动态追踪产品,它可以自动分析运行中的应用,以解决性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+、eBPF+、GDB 和 ODB。

关于作者

章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。

章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!