Blog Logo
  • Home
  • SACC2013
  • Categories
  • Tags
  • About
  • Feed

JVM Instrument使用介绍

by Ruanjf — on java jvm 25 Feb 2018

本文介绍如何使用JVM Attach API在应用运行中修改代码(本文通过jar的方式,如需C\C++的请参考这里,避免应用需要重启启动。使用方式有两种:

  • 命令行接口,在应用启动时添加-javaagent:参数
  • VM 启动后启动代理,在应用启动后获取进程ID(pid),通过Attach API动态加载代码

以下代码在Oracle Java虚拟机环境中使用,主要利用Instrumentation.redefineClasses(ClassDefinition… definitions)实现代码的热更新。为了简化所以代码都整合到一个jar中,具体工程详见GitHub。

先决条件

  • 已安装JDK 6以上版本
  • 已安装Maven
  • (可选)JAVA环境变量classpath已设置tools.jar,如未设置环境变量可参考这里或者在命令行中通过-cp显示设置

打包jar

如果觉得打包太过麻烦,可跳过打包环节直接下载jar

创建一个Maven工程在pom.xml文件中添加tools.jar依赖(由于Maven中默认不包含),依赖中的toolsjar与系统有关系需要使用<profiles>

<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>system</version>
    <scope>system</scope>
    <systemPath>${toolsjar}</systemPath>
</dependency>

...

<profiles>
    <profile>
        <id>windows_profile</id>
        <activation>
            <os>
                <family>windows</family>
            </os>
        </activation>
        <properties>
            <toolsjar>${java.home}/lib/tools.jar</toolsjar>
        </properties>
    </profile>
    <profile>
        <id>Macos_profile</id>
        <activation>
            <os>
                <family>mac</family>
            </os>
        </activation>
        <properties>
            <toolsjar>${java.home}/../lib/tools.jar</toolsjar>
        </properties>
    </profile>
</profiles>

在pom.xml的maven-jar-plugin中添加<Can-Redefine-Classes>true</Can-Redefine-Classes>代理配置允许重定义此代理所需的类,如未设置会导致出现java.lang.UnsupportedOperationException: redefineClasses is not supported错误,详细信息如下:

Exception in thread "Attach Listener" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)
	at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411)
Caused by: java.lang.UnsupportedOperationException: redefineClasses is not supported in this environment
	at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:156)
	at com.runjf.test.jvm.instrument.Util.redefineClasses(Util.java:41)
	at com.runjf.test.jvm.instrument.AgentMain.agentmain(AgentMain.java:19)
	... 6 more
Agent failed to start!

创建一个主类com.runjf.test.jvm.instrument.Main用于测试动态更新代码是否生效。该类的执行逻辑为:打印指定类的指定方法,然后休眠指定的秒数,重复指定的次数。当执行代理后发现先后两次的打印结果不一样则说明动态加载生效了,main的默认打印Demo 1, ret: 1,设置代理后打印Demo 2, ret: 2,结果类似:

Demo 1, ret: 1
AgentMain done: Demo,/Demo.class.2
Demo 2, ret: 2

命令行接口

新建一个类com.runjf.test.jvm.instrument.Premain添加一个静态方法public static void premain(String agentArgs, Instrumentation inst);或者public static void premain(String agentArgs);用于启动时代理的入口,在pom.xml的maven-jar-plugin中添加<Premain-Class>com.runjf.test.jvm.instrument.Premain</Premain-Class>代理类配置

VM 启动后启动代理

新建一个类com.runjf.test.jvm.instrument.AgentMain添加一个静态方法public static void agentmain(String agentArgs, Instrumentation inst);或者public static void agentmain(String agentArgs);用于启动代理的入口,在pom.xml的maven-jar-plugin中添加 <Agent-Class>com.runjf.test.jvm.instrument.AgentMain</Agent-Class> 代理类配置。再添加一个类com.runjf.test.jvm.instrument.AgentAttach用于调用Attach API启动代理。

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
            AgentInitializationException {
        String jarFileName = args[0];
        String processId = args[1];
        VirtualMachine virtualMachine = VirtualMachine.attach(processId);
        try {
            virtualMachine.loadAgent(jarFileName, args[2]);
        } finally {
            virtualMachine.detach();
        }
        System.out.println("AgentAttach done: " + Arrays.toString(args));
    }

需要传入的参数分别为:包含代理代码的jar、已启动的Java进程号和代理启动需要的参数(动态加载的类名,Class所在位置。例如:Demo,/Demo.class.2)

接下来进入jvm-instrument文件夹执行mvn package命令生成jvm-instrument-1.0-SNAPSHOT.jar

执行测试命令

进入target文件夹,执行下面两种测试

  • 命令行接口

    首先,执行命令java -jar jvm-instrument-1.0-SNAPSHOT.jar 6 10 Demo getInt观察默认的结果,每隔10秒打印一次Demo.getInt方法共重复6次,控制台打印结果如下。

      14203@rjf-mba.local
      [6, 10, Demo, getInt]
      Demo 1, ret: 1
      Demo 1, ret: 1
      Demo 1, ret: 1
      Demo 1, ret: 1
      Demo 1, ret: 1
      Demo 1, ret: 1
    

    然后,在命令中添加代理配置java -javaagent:jvm-instrument-1.0-SNAPSHOT.jar=Demo,/Demo.class.2 -jar jvm-instrument-1.0-SNAPSHOT.jar 6 10 Demo getInt,控制台打印结果如下:

      14255@rjf-mba.local
      [6, 10, Demo, getInt]
      Demo 2, ret: 2
      Demo 2, ret: 2
      Demo 2, ret: 2
      Demo 2, ret: 2
      Demo 2, ret: 2
      Demo 2, ret: 2
    

    需要注意下javaagent的参数传递格式-javaagent:<jarpath>[=<选项>]

  • VM 启动后启动代理

    首先,启动默认的jarjava -jar jvm-instrument-1.0-SNAPSHOT.jar 6 10 Demo getInt控制台打印结果如下:

      10153@rjf-mba.local
      [6, 10, Demo, getInt]
      Demo 1, ret: 1
    

    注意,接下来的一步需要tools.jar有些同学的classpath并为包含该jar,可以在命令中显示指定(如:java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/tools.jar)或者在classpath中添加jar路径(如:CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/jre/lib/rt.jar)。

    然后,新开一个终端进入到target文件夹执行java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/lib/tools.jar:jvm-instrument-1.0-SNAPSHOT.jar:. com.runjf.test.jvm.instrument.AgentAttach jvm-instrument-1.0-SNAPSHOT.jar 10153 Demo,/Demo.class.2来启动代理,其中10153为之前启动的java应用进程号,控制台打印结果如下:

      AgentAttach done: [jvm-instrument-1.0-SNAPSHOT.jar, 10153, Demo,/Demo.class.2]
    

    现在回到前一个终端可以看到(可能需要等几秒)如下信息则说明动态加载生效了

      10153@rjf-mba.local
      [6, 10, Demo, getInt]
      Demo 1, ret: 1
      Demo 1, ret: 1
      AgentMain done: Demo,/Demo.class.2
      Demo 2, ret: 2
    

参考文档

  • Attach API
  • Manifest customization
  • Apache Maven
Ruanjf Author

Ruanjf

ruanjiefeng@gmail.com

是我,这就是我

Comments

comments powered by Disqus
All content copyright Ruanjf © 2020 • All rights reserved.
Proudly published with Jekyll on Tuesday, 05 May 2020 at 05:34 PM UTC