JEP 451:准备禁止动态加载代理
原文:https://openjdk.org/jeps/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 agents”)以Java代码编写,也可以使用JVM工具接口(“JVM TI agents”)以本机代码编写。
代理在设计时考虑了良性检测,即添加检测不会影响应用行为。然而,高级开发人员发现,诸如面向切面编程之类的用例会以任意方式改变应用行为。此外,没有什么可以阻止代理更改应用外部的代码,例如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飞行记录器(JFR)。 - 调试器需要在启动时使用
-agentlib:jdwp
选项启用内置于JVM中的代理。然后它们通过某个IPC通道与代理进行通信,但也能够利用Attach API。 - 分析器,以及更普遍的应用性能监控(APM)工具,它们使用在启动时加载的代理来检测应用代码,以便它发出JFR事件供JDK任务控制(JMC)或其他客户端使用。
Attach API还允许工具将代理动态加载到正在运行的JVM中。该功能支持涉及动态更改任意代码的高级用例。动态加载代理的工具示例包括:
- 分析器,连接到正在运行的JVM并动态加载代理以检测应用代码。
- 临时故障排除工具,在运行时读取和写入应用状态。动态加载的代理要么使用JVM TI检查正在运行的程序的状态,要么转换和检测已加载的类。
(非常高级的开发人员有时会通过编写一个代理来修补错误代码,并动态加载该代理来修复生产环境中的错误。但是,这种用法不受支持,并且从未被推荐过。代理重新定义已加载类的能力受到限制,因此通过修补来修复错误的能力是有限的。此外,代理无法保留它所做的更改,因此重新启动应用将恢复更改。)
动态加载的代理赋予可服务性工具更改正在运行的应用的超能力。但是,附加操作是由拥有适当操作系统资格的人工操作员触发的。该人工操作员会批准更改应用,因此可服务性工具不受对其他代码施加的完整性约束。因此,默认情况下允许动态加载代理,但在JDK 9及更高版本中,可以使用命令行上的-XX:-EnableDynamicAgentLoading
选项禁止动态加载代理。
代理与库
尽管库和工具在概念上的关注点不同,但有些库提供的功能依赖于代理所具有的代码修改超能力。例如,模拟库可能会重新定义应用类,以绕过业务逻辑不变量;而白盒测试库可能会重新定义JDK类,以便始终允许对private
字段进行反射。为了获得这些功能,库可以使用代理,该代理从JVM获取功能强大的Instrumentation
对象并将其传送到库。
诸如此类的库通过要求在命令行上使用-javaagent
选项指定库的代理来确保应用所有者批准更改应用。Quasar就是一个这样做的库的示例,它是后来成为虚拟线程(JEP 444)的早期原型。
其他库则采取更可疑的方法,在未经应用所有者批准的情况下获得功能。它们使用Attach API悄悄连接到它们在其中运行的JVM并动态加载代理,实际上伪装成可服务性工具。为了保持完整性,JDK 9及更高版本默认阻止代码连接到当前JVM。(可以通过-Djdk.attach.allowAttachSelf=true
启用此类连接。)然而,事实证明这种措施不够充分:一些库现在会生成第二个JVM,该JVM连接到第一个JVM并在那里加载代理,同时加载库。
如果库使用代理悄悄地重新定义JDK类,从而绕过强封装,那么强封装所强制执行的任何不变性都不再可信。完整性将丧失。
迈向默认完整性
为了确保完整性,我们需要采取更强有力的措施来防止动态加载代理的库被滥用。遗憾的是,我们还没有找到一种简单而自动的方法来区分动态加载代理的服务性工具和动态加载代理的库。让工具自由发挥就意味着让库自由发挥,这等于放弃默认完整性。
因此,我们建议要求动态加载代理必须得到应用所有者的批准——就像我们自JDK 5以来就要求启动时加载代理必须得到应用所有者的批准一样。这一变化将使Java平台更接近长期的默认完整性愿景。实际上,应用所有者必须选择通过命令行选项来允许动态加载代理。
幸运的是,大多数可服务性工具并不依赖于动态加载的代理。但是,默认情况下不允许动态加载代理,意味着需要动态加载代理的临时故障排除技术将不再立即可用。如果需要动态加载代理,则必须使用适当的命令行选项重新启动JVM,以授予应用来自其所有者的批准。
由于大多数现代服务器应用都设计有冗余,因此可以根据需要使用命令行选项重新启动各个节点,因此这一变化的影响将得到缓解。特殊情况(例如,绝不能因维护而停止的JVM,或需要密切观察新软件版本的金丝雀进程)通常可以提前识别,以便从一开始就启用代理的动态加载。
要求应用所有者批准动态加载的代理,将允许Java生态系统实现默认完整性的愿景,而不会实质性地限制可服务性。
描述
在JDK 21中,允许动态加载代理,但发生这种情况时JVM会发出警告。例如:
为了允许工具动态加载代理而不出现警告,用户必须在命令行上使用-XX:+EnableDynamicAgentLoading
选项运行。
使用-Djdk.instrument.traceUsage
运行会导致java.lang.instrument
API的方法在使用时打印一条消息和堆栈跟踪。这有助于识别错误地使用动态加载代理,而不是启动时加载代理的库。鼓励动态加载代理的库的维护者更新其文档,以描述用户如何在启动时加载代理;各种部署选项由java.lang.instrument API提供。
在将来的某个版本中,默认情况下将不允许动态加载代理。在外部,任何使用Attach API动态加载代理的行为都将导致抛出异常:
为了允许动态加载代理(默认情况下不允许),用户必须在命令行上使用-XX:+EnableDynamicAgentLoading
运行。
为了为未来版本中更改的默认值做准备,JDK 9或任何更高版本的用户可以通过在命令行上运行-XX:-EnableDynamicAgentLoading
来明确禁止代理的动态加载。
使用启动时加载的代理的工具不受这些更改的影响。-javaagent
选项、-agentlib
选项和Launcher-Agent-Class
JAR文件属性的含义和操作均保持不变。
除了动态加载代理之外,使用Attach API的工具不会受到这些更改的影响。
历史回顾
默认禁止动态加载代理最初是在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来赋予自己更多权力。 - 采用区分人工操作工具和伪装成工具的库的身份验证机制,默认情况下,允许工具动态加载代理而不发出警告,但当库尝试动态加载代理时发出警告。
我们已经探索了几种类似的方法,但这些方法要么很复杂,要么需要在命令行上进行特殊设置,这不会减少对动态加载代理的工具的影响。