JProfiler 让 AI 智能体得以有效利用堆转储
JProfiler 16.3 通过其 MCP 服务器将 JProfiler 的所有主要子系统开放给 AI 智能体。其中最实用的新增功能之一是堆转储分析。智能体现在可以检查堆中的对象、追踪它们的引用,并查明它们为何未被垃圾回收。
没有运行时数据,AI 智能体就无法看到哪些对象仍然存活,也不知道是什么使它们保持可达。JProfiler 填补了这一空白,使智能体能够从真实证据出发,并据此找到相关的源代码。
一个真实的支持案例
16.3 发布后不久,我们收到了另一个产品 install4j 的一份支持工单,报告在更新检查期间出现内存泄漏。客户附上了一份在 JVM 内存耗尽时写入的 HPROF 堆转储。我们让一个智能体调查这个问题,并在可能的情况下修复它。该智能体可以使用多个 MCP 服务器,其中包括全新的 JProfiler MCP 服务器。
该应用程序是一个后台进程,通过在进程内调用 install4j 更新下载器来反复执行更新检查。它的最大堆大小为 64 MB。最终,反复的更新检查耗尽了可用内存,导致应用程序失败。
与常见情形一样,修复本身很小,却难以找到。这次调查堪称利用运行时数据指导源码检查的教科书式范例。整个过程可以分为五个步骤来讲述:
技术细节
该智能体只需要三个 MCP 工具:load_snapshot、check_status 和 query。前两个工具用于加载快照并报告可用的数据集。query 工具为处理性能分析数据提供了一种低代码模式。每个数据集都以 JSON 形式表示。智能体提供一个 jq(一种 JSON 查询语言)表达式,用来选取所需的数据并将其转换为紧凑的结果。过滤、排序和投影都在 JProfiler 中进行,因此只有这个结果会返回给智能体。
加载堆转储
load_snapshot 接受 JProfiler 快照、HPROF 堆转储和 JFR 记录。快照加载是异步进行的。智能体会调用 check_status 来了解数据何时就绪,以及哪些数据集和操作可用。
HPROF 堆转储特别有用,因为应用程序在运行时无需挂载性能分析代理。您可以使用 jcmd 从正在运行的 HotSpot JVM 触发堆转储,也可以通过在应用程序命令行中添加 -XX:+HeapDumpOnOutOfMemoryError VM 参数,让 JVM 在发生 OutOfMemoryError 时自动写入堆转储。
对于这份 HPROF 文件,数据目录中包含了类直方图、最大对象、实例、大小和检查等堆数据集。这份转储共有 1,385,451 个对象,总大小为 63 MiB。普通的 HPROF 文件不含分配堆栈跟踪或性能记录,因此分析只能从转储中捕获的对象图入手。
发现重复的安装器运行
智能体从 heap.classes 数据集入手,筛选出 com.install4j 包。初始结果以及随后的一次整堆查询显示,安装器 UI 和配置对象出现了令人意外的堆积:
| 类 | 实例数 |
|---|---|
InstallerWizardScreenExecutor |
34 |
InstallerContextImpl |
34 |
DeferredDocumentImpl |
34 |
FormPanel |
198 |
ActionBeanConfig |
2,686 |
如此吻合的计数表明,这些对象来自反复执行的安装器运行。每次进程内启动都会创建一个执行器、一个安装器上下文和一个解析后的配置文档,按理只应保留来自当前启动的对象。然而实际上,34 次完整的安装器运行仍被保留着。
heap.biggestObjects 数据集提供了另一个线索:许多安装器上下文的保留大小恰好都是 74,800 字节。如此一致的保留大小往往意味着同一个对象图被重复复制了多份。此外,34 个已解析的 XML 文档各自保留了 700 到 750 KB 的内存。
追踪通往 GC 根的路径
找到可疑对象只是第一步,接下来要弄清的是它们为何仍然可达。智能体通过 heap.instances 选中了一个 InstallerContextImpl 实例,并在其 jq 表达式中调用了 pathToRoot 内置函数:
query({
dataset: "heap.instances",
class: "com.install4j.runtime.installer.InstallerContextImpl",
expression: `
.instances[0]
| {
id,
keptAliveBy:
pathToRoot(.id).paths[0].path
| map({class, via, root})
}
`
})
返回的路径包含了整条泄漏链:
LookAndFeelChangeUtil.lookAndFeelChangeHandlers (static) → WeakHashMap entry value → look-and-feel handler registration → captured lambda → WidgetStyleHandlerImpl → FormEnvironmentImpl → FormPanel → InstallerContextImpl
LookAndFeelChangeUtil 中的一个静态字段持有该 Map,其值是处理器注册项,这些注册项的 lambda 引用了表单环境和安装器上下文。名为 arg$1 的自动生成 lambda 字段尤其有用,因为它们标识了被捕获的变量。至此,堆中的证据已经确定了需要检查的确切源代码结构。
为什么 WeakHashMap 未能防止泄漏
来自其他已解析文档以及顶层执行器的路径都指向同一个静态 Map。此外,referrersByClass 按类聚合了所有传入引用。这证实了外观注册项正是使所有这些对象保持可达的共同来源。
WeakHashMap 中的条目只有在其键不再强可达时才会被移除。而在这个对象图中,每个条目的值都间接引用了它自己的键:
Map 的键是被设置样式的 Swing 组件,值则是处理器注册项。注册项中被捕获的 lambda 保留了表单面板,而表单面板又保留了它的子组件,其中包括 Map 的键。由于该 Map 由静态字段持有,这条从 Map 值指回 Map 键的强引用路径导致所有条目都无法被移除。
源代码显示,这种捕获模式出现在 WidgetStyleHandlerImpl 中:设置样式的 lambda 调用了诸如 addStyleColor 之类的实例方法,而这些方法捕获了处理器实例及其表单环境。
修复泄漏
样式辅助方法被改成了 static 方法,这样设置样式的 lambda 便不再捕获处理器。由于失去了对处理器的强引用,这些注册项也不再保留表单面板或 Map 键。
智能体添加了一个使用 WeakReference 的回归测试,用于验证外观注册项不会保留处理器。该测试在原始代码上失败,而在应用修复后通过。完整的测试套件也全部通过,修复工作就此完成。
仅凭源代码,智能体能否发现这个泄漏?
AI 智能体非常擅长阅读源代码并识别可能导致性能问题或资源泄漏的异常。然而,当代码库的规模超出智能体的上下文窗口时,这种方法就不再适用。此时,智能体需要能够检查运行时数据的工具。
在本案例中,更新检查是怀疑对象。然而,阅读更新检查的源代码并不会发现真正的泄漏。泄漏发生在每次进程内启动时都会初始化的组件样式代码中。智能体利用被保留的对象及其引用链,定位到了相关的源代码。
这就是让 AI 智能体访问性能分析器的主要好处。性能分析器提供了来自应用程序的证据,使智能体能够将源码分析集中在真正引发问题的代码上。
开始使用
面向 AI 智能体的堆转储分析功能已在 JProfiler 16.3 中提供。要让您的智能体获得这一能力,请访问 JProfiler agents 页面,并在终端中执行其中的一行命令。