JEP 451:准备禁止动态加载代理
总结
当代理被动态加载到正在运行的 JVM 中时发出警告。这些警告旨在让用户为未来的版本做好准备,在该版本中为了默认提高完整性将默认禁止动态加载代理。在启动时加载代理的服务工具不会导致任何版本发出警告。
目标
-
为未来版本的 JDK 做好准备,该版本默认情况下将禁止将代理加载到正在运行的 JVM 中。
-
重新评估可服务性(涉及对运行代码的临时更改)与完整性(假定运行代码不会被任意更改)之间的平衡。
-
确保大多数不需要动态加载代理的工具不受影响。
-
将动态加载代理的能力与其他所谓的“超级能力”(如 深度反射)对齐。
非目标
-
本提案的目标并非防止在 JVM 启动时通过
-javaagent
或-agentlib
命令行选项加载代理,也不会在使用这些选项时发出警告。 -
本提案的目标不是废弃或移除 Attach API 中动态加载代理的部分;目标只是为默认情况下禁止使用这些功能做好准备。
-
本提案并不旨在更改 Attach API 中允许可维护性工具连接到正在运行的 JVM 以进行监控和管理的部分。像
jcmd
和jconsole
这样的工具将继续无需命令行选项即可正常工作,并且不会发出警告。
动机
Java 平台中的代理
代理(agent)是一种可以在应用程序运行时更改其代码的组件。代理最早由 JDK 5 中的 Java 平台分析架构 引入,作为一种让工具(尤其是性能分析器)对类进行 插桩(instrument)的方式。这意味着在不改变代码原有行为的前提下,修改类的代码以使其能够向应用程序外部的工具发出事件。代理通过在类加载期间转换类,或者重新定义先前已加载的类来实现这一功能。代理可以使用 java.lang.instrument
API 用 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 的工具示例包括:
-
监控和管理工具,例如
jcmd
和jconsole
,它们可以观察应用程序指标并更改配置。例如,如果某个应用程序使用了java.util.logging
API,那么操作员可以使用jconsole
动态调整日志级别。这些工具利用了专门的jcmd
协议、JMX 以及 JDK Flight Recorder (JFR)。 -
调试器,需要在 JVM 中内置一个代理,并在启动时通过
-agentlib:jdwp
选项启用。然后,它们通过某种 IPC 通道与代理通信,但也可以利用 Attach API。 -
性能分析器,以及更广泛的 Application Performance Monitoring(APM,应用性能监控)工具,它们使用在启动时加载的代理来对应用程序代码进行插桩,以便发出 JFR 事件供 JDK Mission Control 或其他客户端使用。
Attach API 还允许工具动态地将代理加载到正在运行的 JVM 中。此功能支持涉及动态更改任意代码的高级用例。动态加载代理的工具示例包括:
-
Profilers,它们连接到正在运行的 JVM,并动态加载代理以对应用程序代码进行检测。
-
临时故障排查工具,它们在运行时读取和写入应用程序状态。动态加载的代理要么使用 JVM TI 检查正在运行的程序的状态,要么转换和检测已加载的类。
(非常高级的开发者有时会通过编写一个代理来修复生产环境中的错误,这个代理会修补有问题的代码并动态加载该代理。然而,这不是一个被支持的用例,也从未被推荐过。代理重新定义已加载类的能力受到限制,因此通过修补来修复错误的能力是有限的。此外,代理无法持久化其做出的更改,因此重启应用程序将撤销该更改。)
动态加载的代理程序赋予了可维护性工具改变正在运行的应用程序的超级能力。然而,附加工具是由具有适当操作系统凭据的操作员触发的。此操作员在循环中授予更改应用程序的批准,因此可维护性工具不受对其他代码施加的完整性约束的限制。因此,默认情况下允许动态加载代理程序,不过在 JDK 9 及以上版本中,可以使用命令行中的 -XX:-EnableDynamicAgentLoading
选项来禁止该功能。
代理和库
尽管库和工具在概念上是分离的,但一些库提供了依赖于代理代码修改能力的功能。例如,一个模拟库可能会重新定义应用类以绕过业务逻辑不变量,而白盒测试库可能会重新定义 JDK 类,以便始终允许对 private
字段进行反射操作。为了获得这些能力,库可以使用一个代理,该代理从 JVM 获取一个全能的 Instrumentation
对象,并将其传递给库。
其他库则采取一种更为可疑的方法,在未经应用所有者许可的情况下获取功能。它们使用 Attach API 默默连接到其运行所在的 JVM,并动态加载代理,实际上伪装成可服务性工具。为了保持完整性,JDK 9 及之后的版本默认阻止代码连接到当前的 JVM。(可以通过 -Djdk.attach.allowAttachSelf=true
启用这种连接。)然而,这一措施已被证明是不够的:一些库现在会生成第二个 JVM,该 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
选项运行。
使用 -Djdk.instrument.traceUsage
运行会导致 java.lang.instrument
API 的方法在被使用时打印一条消息和堆栈跟踪。这有助于识别错误地使用动态加载代理的库,而不是在启动时加载的代理。鼓励动态加载代理的库的维护者更新其文档,以描述用户如何在启动时加载代理;各种部署选项由 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
运行。
为了为未来版本中默认值的更改做好准备,JDK 9 或任何更高版本的用户可以通过在命令行中使用 -XX:-EnableDynamicAgentLoading
明确禁止动态加载代理。
这些更改不会影响在启动时加载代理的工具。 -javaagent
选项、-agentlib
选项和 Launcher-Agent-Class
JAR 文件属性的含义和操作保持不变。
对于使用 Attach API 的工具,如果其目的不是为了动态加载代理,则不受这些更改的影响。
库不得动态加载代理。使用代理的库必须在启动时通过 -javaagent
/-agentlib
选项加载它。
历史注释
默认情况下禁止动态加载代理程序的提议最初是在 2017 年提出的,作为在 JDK 9 中向平台添加模块的一部分。该提议的内容是:
在未来的版本中,JVM TI 代理的动态加载将默认被禁用。为了应对这一变化,我们建议允许动态代理的应用程序开始使用选项
-XX:+EnableDynamicAgentLoading
来显式启用该加载功能。
2017 年的共识是将更改从 JDK 9 推迟到以后的版本,以便工具维护者有时间通知其用户。然而,过去当我们加强封装时,我们会在前一个版本中发出警告,以提高对即将到来的更改的认识。本 JEP 遵循相同的程序。
风险与假设
-
我们假设大多数可服务性场景涉及使用
jcmd
、jconsole
、调试器、JFR 和 APM 工具,这些工具不会动态加载代理,因此不会受到影响。 -
我们假设动态加载代理的库维护者将更新其文档,要求应用程序所有者在启动时使用
-javaagent
选项加载代理,或者通过-XX:+EnableDynamicAgentLoading
选项启用动态加载代理。
未来工作
-
用于分析原生代码的高级分析器仅仅使用 JVM TI 代理来获取对 HotSpot 内部机制的访问权限,这些机制可以支持性能分析。在生产环境中对应用程序进行性能分析时,它们可能会动态加载代理。这一使用场景最好通过扩展 JFR 的功能来解决,使其完全无需依赖代理即可完成任务。JFR 能够与 HotSpot 的 JIT 编译器协同工作,从而比通过 JVM TI API 或高级分析器常用的内部未公开方法
AsyncGetCallTrace
捕获大批量的堆栈跟踪信息高效得多。 -
一些有趣的代码操作使用场景可以通过直接向库提供一个封装性受尊重的
Instrumentation
对象(无需代理参与)来实现。这将允许库转换或重新定义对其模块开放的模块中的类。
替代方案
-
默认情况下,仅在动态加载原生 JVM TI 代理时发出警告,并默认限制动态加载的 Java 代理的功能(即,当未指定
-XX:+EnableDynamicAgentLoading
选项时),使得当这些代理尝试修改命名模块中的类时发出警告,而允许它们在不发出警告的情况下修改未命名模块中的类。此方法更为复杂,并且不能支持更多实用的工具代理。此外,它并不能阻止 Java 代理通过 JNI 授予自身更多权限。
-
采用一种身份验证机制,区分人工操作的工具和伪装成工具的库,默认情况下允许工具在不发出警告的情况下动态加载代理,而当库尝试动态加载代理时发出警告。
我们已经探索了几种类似的方法,但要么过于复杂,要么需要在命令行中进行特殊设置,这并不会减少对动态加载代理的工具的影响。