JVM 热更新入门(暂停)

引言

最近在同事的安利下,尝试接触了阿里的开源工具 Arthas。

在学习并执行 redefine 命令时对背后的逻辑感到十分好奇,之前也认识到 JVM 的高自由度,但是从来没有尝试过去使用相关的接口。于是上网搜索,找到博客一篇 —— 《手把手教你实现热更新功能》。马上仿写了个 DEMO 跑起来,代码量也不多,可以参考原文也可以跳过,这里为防止之后遗忘,简单提一下大致内容和要点。

  • 创建 Spring Web 项目,监听 “/hello” 路径,并返回 “Hello World”(这里随便弄,主要是为了回显信息,方便观察热更新是否成功) ,通过 getRuntimeMXBean() 方法获取到虚拟机运行时的 Bean 信息,这里只需要用到 PID。
  • 创建热更新项目,其中一个类要实现 public static void agentmain(String args, Instrumentation inst) 方法:
    1. 根据传入的路径,获取到编译后的字节码文件(热更新的字节码)
    2. 使用 asm 读取 bytes
    3. 遍历 inst 已加载的 Classes,匹配热更新的目标 Class
    4. 根据 Class 和 bytes 生成新的 ClassDefinition
    5. 调用 inst.redefineClasses(ClassDefinition definition) 方法,替换当前使用的 Class
  • 热更新项目中,创建主方法(程序入口),主要是为了传入参数(pid, 编译后的字节码文件路径),还要获取当前 jar 包的路径,最关键的一步,通过 VitrualMachine.attach(String id),传入 pid 建立和 JVM 的连接,并调用 loadAgent(String agent, String options) 方法,加载 Agent-Class(也就是上面创建的 agent 类)。
  • 要能够正确找到并加载 Agent-Class,还需要在打包时指定 Agent-Class,并设置 Can-Redefine-Class 权限,使用 jar 包执行时,也需要指定 mainClass。

这里引用 JDK8 的文档说明完成热更新的官方要求:

An implementation may provide a mechanism to start agents sometime after the the VM has started. The details as to how this is initiated are implementation specific but typically the application has already started and its main method has already been invoked. 

程序启动后加载 Agent。

In cases where an implementation supports the starting of agents after the VM has started the following applies:

    1. The manifest of the agent JAR must contain the attribute Agent-Class. The value of this attribute is the name of the agent class.

    Agent Jar 包中的 manifest 必须包含 Agent-Class 属性,其对应值应为 agent 类的名称。

    2. The agent class must implement a public static agentmain method.

    Agent 类必须实现 public static void agentmain() 方法。

    3. The system class loader ( ClassLoader.getSystemClassLoader) must support a mechanism to add an agent JAR file to the system class path.

    系统类加载器必须支持通过系统路径添加 Agent Jar 文件的机制。

Maven 的 POM 配置关键点如下:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <finalName>instrumentDemo</finalName>
                    <appendAssemblyId>false</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <!-- 指定 Agent-Class -->
                            <Agent-Class>com.vaxtomis.instrumentDemo.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        </manifestEntries>
                        <manifest>
                            <!-- 指定入口 -->
                            <mainClass>com.vaxtomis.instrumentDemo.AttachMain</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

参阅注释

上面的 DEMO 主要涉及到 VirtualMachine(attach 下)和 Instrumentation(instrument 下)。

VirtualMachine

A VirtualMachine represents a Java virtual machine to which this Java virtual machine has attached. The Java virtual machine to which it is attached is sometimes called the target virtual machine, or target VM.

VirtualMachine,用于映射和操作连接到的 Java 虚拟机。它所连接的 Java 虚拟机有时称为目标虚拟机或目标 VM。

An application (typically a tool such as a managemet console or profiler) uses a VirtualMachine to load an agent into the target VM. For example, a profiler tool written in the Java Language might attach to a running application and load its profiler agent to profile the running application.

应用程序(通常是诸如控制台或分析器之类的工具)使用 VirtualMachine 将 Agent 加载到目标 VM 中。例如,用 Java 语言编写的分析器工具可以连接到运行中的应用程序,并加载分析器实现的 Agent 来分析程序。

A VirtualMachine is obtained by invoking the attach method with an identifier that identifies the target virtual machine. 
The identifier is implementation-dependent but is typically the process identifier (or pid) in environments where each Java virtual machine runs in its own operating system process. 

VirtualMachine 通过调用 attach 方法并传入目标虚拟机标识符(pid)获得。
该标识符依赖于具体实现,但通常是每个 Java 虚拟机在其操作系统运行环境的进程标识符(或 pid)。

Alternatively, a VirtualMachine instance is obtained by invoking the attach method with a VirtualMachineDescriptor obtained from the list of virtual machine descriptors returned by the list method. 

也可以使用 list 方法,获取虚拟机描述符列表。通过列表中的 VirtualMachineDescriptor,调用 attach 方法也能得到 VirtualMachine 实例。

Once a reference to a virtual machine is obtained, the loadAgent, loadAgentLibrary, and loadAgentPath methods are used to load agents into target virtual machine. The loadAgent method is used to load agents that are written in the Java Language and deployed in a JAR file. (See java.lang.instrument for a detailed description on how these agents are loaded and started). The loadAgentLibrary and loadAgentPath methods are used to load agents that are deployed either in a dynamic library or statically linked into the VM and make use of the JVM Tools Interface.

一旦获得虚拟机的引用,就可以使用 loadAgent、loadAgentLibrary 和 loadAgentPath 方法将 Agent 加载到目标 VM 中。
loadAgent 方法用于加载由 Java 语言编写并部署在 JAR 文件中的 Agent。loadAgentLibrary 和 loadAgentPath 方法用于加载部署在动态库中或静态链接到 VM 中并使用 JVM 工具接口的 Agent。
(有关如何加载和启动这些代理的详细说明,请参见 java.lang.instrument)

In addition to loading agents a VirtualMachine provides read access to the system properties in the target VM. This can be useful in some environments where properties such as java.home, os.name, or os.arch are used to construct the path to agent that will be loaded into the target VM.

除了加载代理之外,VirtualMachine 还提供对目标 VM 中系统属性的读取访问权限。这在某些环境中很有用,其中使用 java.home、os.name 或 os.arch 等属性来构建路径,从而加载 Agent 到目标 VM。

VirtualMachine#attach

Attaches to a Java virtual machine.

连接到 目标VM 的方法。

This method obtains the list of attach providers by invoking the AttachProvider.providers() method. It then iterates overs the list and invokes each provider's attachVirtualMachine method in turn. If a provider successfully attaches then the iteration terminates, and the VirtualMachine created by the provider that successfully attached is returned by this method.
If the attachVirtualMachine method of all providers throws AttachNotSupportedException then this method also throws AttachNotSupportedException. This means that AttachNotSupportedException is thrown when the identifier provided to this method is invalid, or the identifier corresponds to a Java virtual machine that does not exist, or none of the providers can attach to it. This exception is also thrown if AttachProvider.providers() returns an empty list.

此方法通过调用 AttachProvider.providers() 方法获取连接提供者列表。然后它遍历列表并依次调用每个提供者的 attachVirtualMachine 方法。如果成功连接,则迭代终止,并且此方法返回由成功连接的提供者创建的 VirtualMachine。
如果所有提供者的 attachVirtualMachine 方法都抛出 AttachNotSupportedException,那么此方法也会抛出 AttachNotSupportedException。若传入的标识符无效,或者标识符对应于不存在的 Java 虚拟机,或者没有提供者可以连接到它,将抛出 AttachNotSupportedException。如果 AttachProvider.providers() 返回一个空列表,也会引发此异常。

VirtualMachine#loadAgentLibrary

Loads an agent library.

A JVM TI client is called an agent. It is developed in a native language. A JVM TI agent is deployed in a platform specific manner but it is typically the platform equivalent of a dynamic library. 

Alternatively, it may be statically linked into the VM. This method causes the given agent library to be loaded into the target VM (if not already loaded or if not statically linked into the VM). It then causes the target VM to invoke the Agent_OnAttach function or, for a statically linked agent named 'L', the Agent_OnAttach_L function as specified in the JVM Tools Interface specification. 

Note that the Agent_OnAttach[_L] function is invoked even if the agent library was loaded prior to invoking this method. The agent library provided is the name of the agent library. It is interpreted in the target virtual machine in an implementation-dependent manner. 

Typically an implementation will expand the library name into an operating system specific file name. For example, on UNIX systems, the name L might be expanded to libL.so, and located using the search path specified by the LD_LIBRARY_PATH environment variable. If the agent named 'L' is statically linked into the VM then the VM must export a function named Agent_OnAttach_L. If the Agent_OnAttach[_L] function in the agent library returns an error then an com.sun.tools.attach.AgentInitializationException is thrown. The return value from the Agent_OnAttach[_L] can then be obtained by invoking the returnValue method on the exception.

JVM TI 客户端称为 Agent。它是用原生语言开发的。 JVM TI Agent 以特定于平台的方式部署,但它通常是平台等效的动态库。

它可以静态链接到 VM 中。此方法将给定代理库加载到目标 VM(如果尚未加载或未静态链接到 VM)。然后它会导致目标 VM 调用 Agent_OnAttach 函数,或者对于名为 "L" 的静态链接 Agent 调用 JVM 工具接口规范中指定的 Agent_OnAttach_L 方法。

请注意,即使在调用此方法之前加载了代理库,也会调用 Agent_OnAttach[_L] 函数。提供的代理库是代理库的名称。它在目标虚拟机中的解释方式依赖于具体实现。

通常,实现会将库名称扩展为操作系统特定的文件名。例如,在 UNIX 系统上,名称  L 可能会扩展为 libL.so(增加前缀 lib 和后缀 .so),并使用 LD_LIBRARY_PATH 环境变量指定的搜索路径进行定位。如果名为 "L" 的代理静态链接到 VM,则 VM 必须导出名为 Agent_OnAttach_L 的函数。如果代理库中的 Agent_OnAttach[_L] 函数返回错误,则会引发 AgentInitializationException。然后可以通过对异常调用 returnValue 方法来获取 Agent_OnAttach[_L] 的返回值。

VirtualMachine#loadAgentPath

Load a native agent library by full pathname.

A JVM TI client is called an agent. It is developed in a native language. A JVM TI agent is deployed in a platform specific manner but it is typically the platform equivalent of a dynamic library. 

Alternatively, the native library specified by the agentPath parameter may be statically linked with the VM. The parsing of the agentPath parameter into a statically linked library name is done in a platform specific manner in the VM. For example, in UNIX, an agentPath parameter of /a/b/libL.so would name a library 'L'.

See the JVM TI Specification for more details. This method causes the given agent library to be loaded into the target VM (if not already loaded or if not statically linked into the VM). It then causes the target VM to invoke the Agent_OnAttach function or, for a statically linked agent named 'L', the Agent_OnAttach_L function as specified in the JVM Tools Interface specification. 

Note that the Agent_OnAttach[_L] function is invoked even if the agent library was loaded prior to invoking this method. The agent library provided is the absolute path from which to load the agent library. 

Unlike loadAgentLibrary, the library name is not expanded in the target virtual machine. If the Agent_OnAttach[_L] function in the agent library returns an error then an AgentInitializationException is thrown. The return value from the Agent_OnAttach[_L] can then be obtained by invoking the returnValue method on the exception.

通过绝对路径加载原生代理库

JVM TI 客户端称为 Agent。它是用原生语言开发的。 JVM TI Agent 以特定于平台的方式部署,但它通常是平台等效的动态库。

由 agentPath 参数指定的本机库可以与 VM 静态链接。将 agentPath 参数解析为静态链接库名称是在 VM 中以特定于平台的方式完成的。例如,在 UNIX 中,/a/b/libL.so 的 agentPath 参数会将库命名为 "L"。
此方法将给定代理库加载到目标 VM(如果尚未加载或未静态链接到 VM)。然后它会导致目标 VM 调用 Agent_OnAttach 函数,或者对于名为 "L" 的静态链接代理,调用 JVM 工具接口规范中指定的 Agent_OnAttach_L 函数。

请注意,即使在调用此方法之前加载了代理库,也会调用 Agent_OnAttach[_L] 函数。提供的代理库是加载代理库的绝对路径。与 loadAgentLibrary 不同,库名称不会在目标虚拟机中展开。如果代理库中的 Agent_OnAttach[_L] 函数返回错误,则会引发 AgentInitializationException。然后可以通过对异常调用 returnValue 方法来获取 Agent_OnAttach[_L] 的返回值。

(有关更多详细信息,请参阅 JVM TI 规范)

Instrument

This class provides services needed to instrument Java programming language code. 

此类提供检测 Java 编程语言代码所需的服务

Instrumentation is the addition of byte-codes to methods for the purpose of gathering data to be utilized by tools. Since the changes are purely additive, these tools do not modify application state or behavior. Examples of such benign tools include monitoring agents, profilers, coverage analyzers, and event loggers.


Instrumentation 是在方法中添加字节码,用于收集工具所需的数据。由于对字节码的改动是附加的,因此这些工具不会修改应用程序的状态或行为。这种良性工具的示例包括监控代理、分析器、覆盖分析器和事务 logger。

There are two ways to obtain an instance of the Instrumentation interface:
    1. When a JVM is launched in a way that indicates an agent class. In that case an Instrumentation instance is passed to the premain method of the agent class.
    2. When a JVM provides a mechanism to start agents sometime after the JVM is launched. In that case an Instrumentation instance is passed to the agentmain method of the agent code.

这里有获得 Instrumentation 接口实例的两种方式:
    1. 当 JVM 以指定 Agent 的方式启动时,在这种情况下,Instrumentation 接口的一个实例会传递给 Agent 的 premain 方法
    2. 当 JVM 提供了在 JVM 运行时启动代理的机制时,在这种情况下,一个 Instrumentation 实例被传递给 Agent 的 agentmain 方法。

Instrument#redefineClasses

Redefine the supplied set of classes using the supplied class files.


使用提供的类文件重新定义提供的类集。

This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation)  retransformClasses should be used. 


此方法用于在不参考现有类文件字节的情况下替换类的定义,就像在从源代码重新编译以进行修复并继续调试时所做的那样。如果要转换现有的类文件字节(例如在字节码检测中),则应使用 retransformClasses。

This method operates on a set in order to allow interdependent changes to more than one class at the same time (a redefinition of class A can require a redefinition of class B). 


此方法对集合进行操作,以便允许同时对多个类进行相互依赖的更改(重新定义 A 类可能需要重新定义 B 类)。

If a redefined method has active stack frames, those active frames continue to run the bytecodes of the original method. The redefined method will be used on new invokes. 


如果重新定义的方法持有活跃的堆栈帧,则这些活跃帧继续运行原始方法的字节码。重新定义的方法将用于新的调用。

This method does not cause any initialization except that which would occur under the customary JVM semantics. In other words, redefining a class does not cause its initializers to be run. The values of static variables will remain as they were prior to the call. Instances of the redefined class are not affected. The redefinition may change method bodies, the constant pool and attributes (unless explicitly prohibited). 


除了在惯用的 JVM 语义下发生的初始化之外,此方法不会导致任何初始化。重新定义一个类不会导致它的初始化程序运行。静态变量的值将保持调用前的状态。重新定义的类的实例不受影响。重新定义可能会改变方法体、常量池和属性(除非明确禁止)。

The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. The redefinition must not change the NestHost or NestMembers attributes. These restrictions may be lifted in future versions.

重新定义不得添加、删除或重命名字段或方法,更改方法的签名或更改继承。重新定义不得更改 NestHost 或 NestMembers 属性。这些限制可能会在未来的版本中取消。

The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception. If this method throws an exception, no classes have been redefined. This method is intended for use in instrumentation, as described in the instrumentation class specification.

在应用转换之前,不会检查、验证和安装类文件字节,如果结果字节有误,此方法将引发异常。如果此方法抛出异常,则没有重新定义任何类。此方法旨在用于 Instrumentation,如 Instrumentation 类规范中所述。