跳到主要内容

JEP 451:准备禁止动态加载代理

概括

当代理动态加载到正在运行的 JVM 中时发出警告。这些警告旨在帮助用户为将来的版本做好准备,该版本默认情况下不允许动态加载代理,以提高默认情况下的完整性。在启动时加载代理的可服务性工具不会导致在任何版本中发出警告。

目标

  • 为 JDK 的未来版本做好准备,默认情况下,该版本将禁止将代理加载到正在运行的 JVM 中。

  • 重新评估可服务性(涉及对运行代码的临时更改)和完整性(假设运行代码不会被任意更改)之间的平衡。

  • 确保大多数不需要动态加载代理的工具不受影响。

  • 将动态加载代理的能力与其他所谓的“超级”能力(例如深度反射)结合起来。

非目标

  • 目的不是阻止在 JVM 启动时通过-javaagent-agentlib命令行选项加载代理,也不是在此类使用时发出警告。

  • 我们的目标不是弃用或删除Attach API中动态加载代理的部分;这只是一个目标,为默认情况下禁止使用它们做好准备。

  • 更改 Attach API 的部分并不是目标,这些部分允许可服务性工具连接到正在运行的 JVM 以进行监视和管理。诸如jcmd和 之类的工具jconsole将在没有命令行选项和警告的情况下继续工作。

动机

Java 平台中的代理

_代理_是一个可以在应用程序运行时更改应用程序代码的组件。 JDK 5 中的Java 平台分析架构引入了代理,作为工具(尤其是分析器)_检测类_的一种方式。这意味着更改类中的代码,以便它发出由应用程序外部的工具使用的事件,而无需更改代码的行为。代理通过在类加载期间转换类或通过重新定义先前加载的类来实现此目的。它们可以使用java.lang.instrumentAPI(“Java 代理”)以 Java 代码编写,也可以使用JVM 工具接口(“JVM TI 代理”)以本机代码编写。

代理在设计时考虑了良性检测,其中添加检测不会影响应用程序行为。然而,高级开发人员发现诸如面向方面编程之类的用例可以以任意方式改变应用程序行为。也没有什么可以阻止代理更改应用程序外部的代码,例如 JDK 本身中的代码。为了确保应用程序所有者批准使用代理,JDK 5 要求在命令行上使用-javaagent-agentlib选项指定代理,并在启动时立即加载代理。这代表应用程序所有者明确授予权限。

可维护性和动态加载代理

_可维护性_是系统操作员在应用程序运行时对其进行监视、观察、调试和故障排除的能力。 Java 平台出色的可服务性长期以来一直令人自豪。

为了支持可服务性工具,JDK 6 引入了Attach API。 Attach API 不是 Java 平台的一部分,而是支持外部使用的 JDK API。它允许使用适当的操作系统权限启动的工具连接到本地或远程正在运行的 JVM,并与该 JVM 通信以观察和控制其操作。 Attach API 默认启用,但可以使用-XX:+DisableAttachMechanism命令行上的选项禁用。

使用 Attach API 的工具示例包括:

  • 监视和管理工具,例如jcmdjconsole,可观察应用程序指标并更改配置。例如,如果应用程序使用java.util.loggingAPI,则操作员可以使用该 APIjconsole来动态更改日志级别。这些工具利用专门的jcmd协议、JMXJDK Flight Recorder (JFR)

  • 调试器,需要在启动时使用该-agentlib:jdwp选项启用内置于 JVM 中的代理。然后,它们通过某些 IPC 通道与代理进行通信,但也能够利用 Attach API。

  • 探查器,以及更常见的应用程序性能监控(APM) 工具,它们使用启动时加载的代理来检测应用程序代码,以便发出 JFR 事件以供JDK Mission Control或其他客户端使用。

Attach API 还允许工具将代理动态加载到正在运行的 JVM 中。此功能支持涉及动态更改任意代码的高级用例。动态加载代理的工具示例包括:

  • 探查器,连接到正在运行的 JVM 并动态加载代理以检测应用程序代码。

  • 临时故障排除工具,可在运行时读取和写入应用程序状态。动态加载的代理要么使用 JVM TI 检查正在运行的程序的状态,要么转换和检测加载的类。

(非常高级的开发人员有时会通过编写一个代理来修补有缺陷的代码并动态加载该代理来修复生产环境中的错误。但是,这不是受支持的用例,也从未被推荐过。代理重新定义加载的类的能力受到限制,因此通过修补修复错误的能力是有限的,此外,代理无法保留其所做的更改,因此重新启动应用程序将恢复更改。)

动态加载的代理为可服务性工具提供了改变正在运行的应用程序的超能力。然而,附加工具是由具有适当操作系统凭证的操作员触发的。该循环中的人员授予更改应用程序的批准,因此可服务性工具不受强加于其他代码的完整性约束的约束。因此,默认情况下允许动态加载代理,但在 JDK 9 及更高版本中可以通过-XX:-EnableDynamicAgentLoading命令行上的选项禁止动态加载。

代理和图书馆

尽管库和工具之间的关注点在概念上是分离的,但某些库提供的功能依赖于向代理提供的代码更改超级能力。例如,模拟库可能会重新定义应用程序类以绕过业务逻辑不变量,而白盒测试库可能会重新定义 JDK 类,以便private始终允许对字段进行反射。为了获得这些功能,库可以使用代理Instrumentation从 JVM获取全能对象并将其传送到库。

一些此类库通过要求在命令行上使用该选项指定库的代理来确保应用程序所有者批准更改应用程序-javaagent。执行此操作的库的一个示例是Quasar ,它是后来成为虚拟线程(JEP 444)的早期原型。

其他库则采取更可疑的方法,在未经应用程序所有者批准的情况下获取功能。它们使用 Attach API 以静默方式连接到它们在其中动态运行和加载代理的 JVM,实际上伪装成可服务性工具。为了保持完整性,JDK 9 及更高版本默认情况下会阻止代码连接到当前 JVM。 (此类连接可以通过 启用-Djdk.attach.allowAttachSelf=true。)然而,事实证明该措施是不够的:一些库现在生成第二个 JVM,它连接到第一个 JVM,并在库旁边加载代理。

如果一个库使用代理默默地重新定义 JDK 类,从而绕过强封装,那么强封装强制执行的任何不变量都不可信。诚信丧失了。

默认走向诚信

为了确保完整性,我们需要采取更强有力的措施来防止动态加载代理库的误用。不幸的是,我们还没有找到一种简单且自动的方法来区分动态加载代理的可服务性工具和动态加载代理的库。给予工具自由就意味着给予图书馆自由,这等于默认放弃完整性。

因此,我们建议要求代理的动态加载得到应用程序所有者的批准 - 就像我们自 JDK 5 以来要求代理的启动时加载得到应用程序所有者的批准一样。这一更改将使 Java 平台更接近于长远眼光默认诚信。实际上,应用程序所有者必须选择允许通过命令行选项动态加载代理。

幸运的是,大多数可维护性工具并不依赖于动态加载的代理。但是,默认情况下禁止动态加载代理意味着需要动态加载代理的临时故障排除技术将不再开箱即用。如果需要动态加载代理,则必须使用适当的命令行选项重新启动 JVM,以获得应用程序所有者的批准。

大多数现代服务器应用程序都采用冗余设计,因此可以根据需要使用命令行选项重新启动各个节点,从而减轻此更改的影响。特殊情况——例如永远不能停止维护的 JVM,或者需要密切观察的新软件版本的金丝雀进程——通常可以提前识别,以便从一开始就启用代理的动态加载。

要求应用程序所有者批准动态加载代理将使 Java 生态系统默认实现完整性愿景,而不会严重限制可服务性。

描述

在 JDK 21 中,允许动态加载代理,但 JVM 在发生这种情况时会发出警告。例如:

WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release

要允许工具动态加载代理而不发出警告,用户必须-XX:+EnableDynamicAgentLoading在命令行上使用该选项运行。

运行 with-Djdk.instrument.traceUsage会导致 API 的方法java.lang.instrument在使用时打印消息和堆栈跟踪。这有助于识别错误地使用动态加载代理而不是在启动时加载代理的库。鼓励动态加载代理的库的维护者更新其文档以描述用户如何在启动时加载代理;java.lang.instrument API提供了各种部署选项。

在未来的某些版本中,默认情况下将不允许动态加载代理。开箱即用,任何使用 Attach API 动态加载代理的行为都会导致抛出异常:

com.sun.tools.attach.AgentLoadException: Failed to load agent library: \
Dynamic agent loading is not enabled. Use -XX:+EnableDynamicAgentLoading \
to launch target VM.

要允许动态加载代理(默认情况下不允许),用户必须-XX:+EnableDynamicAgentLoading在命令行上运行 with。

-XX:-EnableDynamicAgentLoading为了为未来版本中更改的默认值做好准备,JDK 9 或任何更高版本的用户可以通过在命令行上运行 with 来明确禁止动态加载代理。

使用启动时加载的代理的工具不受这些更改的影响。-javaagent选项、-agentlib选项和JAR 文件属性的含义和操作Launcher-Agent-Class保持不变。

将 Attach API 用于动态加载代理以外目的的工具不受这些更改的影响。

库不得动态加载代理。使用代理的库必须在启动时使用-javaagent/-agentlib选项加载它。

历史记录

默认情况下禁止动态加载代理最初于 2017 年提出,作为向 JDK 9 中的平台添加模块的一部分。该提案是:

在未来的版本中,JVM TI 代理的动态加载将默认禁用。为了准备该更改,我们建议允许动态代理的应用程序开始使用该选项-XX:+EnableDynamicAgentLoading来显式启用该加载。

2017 年的共识是将 JDK 9 的更改推迟到更高版本,以便工具维护者有时间通知用户。然而,当我们过去加强封装时,我们会在之前的版本中发出警告,以提高人们对即将发生的变化的认识。此 JEP 遵循相同的程序。

风险和假设

  • 我们假设大多数可维护性场景涉及使用调试jcmdjconsole、JFR 和 APM 工具,这些工具不会动态加载代理,因此不会受到影响。

  • 我们假设动态加载代理的库的维护者将更新其文档,要求应用程序所有者在启动时使用该选项加载代理-javaagent,或者通过该选项启用代理的动态加载-XX:+EnableDynamicAgentLoading

未来的工作

  • 分析本机代码的高级分析器仅使用 JVM TI 代理来访问可支持分析的内部 HotSpot 机制。在生产中分析应用程序时,他们可能会动态加载代理。解决此用例的最佳方法是扩展 JFR 的功能,使其无需代理即可执行任务。 JFR 能够与 HotSpot 的 JIT 编译器配合捕获大批量堆栈跟踪,其效率远高于通过 JVM TI API 或AsyncGetCallTrace高级分析器常用的内部未记录方法公开的任何内容。

  • 可以通过Instrumentation直接向库提供尊重封装的对象来提供一些有趣的代码操作用例,而无需涉及代理。这将允许库转换或重新定义对该库模块开放的模块中的类。

备择方案

  • 默认情况下仅在动态加载本机 JVM TI 代理时发出警告,并默认限制动态加载 Java 代理的功能(即,当-XX:+EnableDynamicAgentLoading未指定该选项时),以便在尝试修改命名模块中的类时发出警告同时允许他们在没有警告的情况下修改未命名模块中的类。

    这种方法更加复杂并且不支持更实用的工具代理。此外,它并不阻止 Java 代理使用 JNI 来授予自己更多的权力。

  • 采用一种身份验证机制来区分人工操作的工具和伪装成工具的库,默认情况下,允许工具在不发出警告的情况下动态加载代理,但在库尝试动态加载代理时发出警告。

    我们已经沿着这些思路探索了几种方法,但所有方法要么都很复杂,要么需要在命令行上进行特殊设置,这不会减少对动态加载代理的工具的影响。