# javaagent

# 介绍

jdk提供了一种强大的可以对已有class代码进行运行时注入修改的能力。 javaagent可以在启动时通过-javaagent:agentJarPath或运行时attach加载agent包的方式使用,通过javaagent我们可以对特定的类进行字节码修改, 在方法执行前后注入特定的逻辑。 通过字节码修改,可以实现监控tracing、性能分析、在线诊断、代码热更新热部署等等各种能力。

  • 监控tracing: 分布式tracing框架的Java类库(比如skywalking, brave, opentracing-java)常使用javaagent实现,因为tracing需要在各个第三方框架内注入tracing数据的统计收集逻辑,比如要在grpc、kafka中发送消息前后收集tracing日志,但是这些第三方的jar包我们不方便修改它们的代码,使用javaagent就成为了很好的选择。
  • 性能分析: 很多性能分析软件例如jprofiler使用javaagent技术,一般分析分为sampling和instrumentation两种方式,sample是通过类似jstack的方式采集方法的执行栈,instrumentation就是修改字节码来收集方法的执行次数、耗时等信息。
  • 在线诊断: arthas这样的软件使用javaagent技术在运行时将诊断逻辑注入到已有代码中,实现watch,trace等功能
  • 代码热更新、热部署: 通过javaagent技术,还能够实现Java代码的热更新,减少Java服务重启次数,提升开发效率,比如开源的https://github.com/HotswapProjects/HotswapAgent和https://github.com/dcevm/dcevm

# 使用

# 编写、打包、使用javaagent

我们以javaagent-example (opens new window)项目为例使用字节码实现一个最简单的AOP功能,在某个方法执行前打印字符串。

编写javaagent需要在jar包中创建META-INF/MANIFEST.MF来配置agent的入口类等信息,通过maven的maven-assembly-plugin插件把resources文件夹下META-INF/MANIFEST.MF文件打包到jar包中。(

maven pom相关配置示例如下。(除了maven-assembly-plugin,还可以用maven-shade-plugin)

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.0.1</version>
            <executions>
                <execution>
                    <id>attach-sources</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>jar-no-fork</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                </archive>
            </configuration>
            <executions>
                <execution>
                    <id>assemble-all</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
    <resources>
        <resource>
            <directory>${basedir}/src/main/resources</directory>
        </resource>
        <resource>
            <directory>${basedir}/src/main/java</directory>
        </resource>
    </resources>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

同时我们还需要在pom.xml添加我们要使用的字节码修改框架asm

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-all</artifactId>
    <version>5.1</version>
</dependency>
1
2
3
4
5

然后我们添加MANIFEST.MF文件(在resources/META-INF文件夹下,如果没有则进行创建)

Premain-Class和Agent-Class都配置成agent的入口类。Can-Redefine-Classes表示agent是否需要redefine的能力,默认为false,还有一个Can-Retransform-Classes配置, 我们这里虽然声明了true但是其实没有使用redfine能力。

Manifest-Version: 1.0
Premain-Class: com.lzy.javaagent.AgentMain
Agent-Class: com.lzy.javaagent.AgentMain
Can-Redefine-Classes: true
1
2
3
4

最后编写Agent入口类,也就是上面的com.lzy.javaagent.AgentMain

javaagent的核心功能集中在通过premain/agentmain获得的Instrumentation对象上,通过Instrumentation 对象可以添加ClassFileTransformer、调用redefine/retransform方法,以实现修改类代码的能力。 我们要实现的简单的AOP,就是在类加载前,给Instrumentation添加我们的自定义的ClassFileTransformer, ClassFileTransformer读取加载的类,然后通过字节码工具进行解析、修改,在AOP目标类的方法的执行前后打印我们想打印的字符串。 具体实现如下,其中ClassFileTransformer使用javassist框架进行字节码修改,后续的文章我们会详细介绍javassist的使用。

AgentMain接收Instrumentation和String参数,这里我们把String参数用来指定AOP目标类

public class AgentMain {
	public static void premain(String agentOps, Instrumentation inst) {
		instrument(agentOps, inst);
	}

	public static void agentmain(String agentOps, Instrumentation inst) {
		instrument(agentOps, inst);
	}

	/**
	 * agentOps is aop target classname
	 */
	private static void instrument(String agentOps, Instrumentation inst) {
		System.out.println(agentOps);
		inst.addTransformer(new AOPTransformer(agentOps));
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AOPTransformer实现ClassFileTransformer,在加载指定的类时,对类进行修改在方法调用前增加代码,打印方法名。

/**
 * @author liuzhengyang
 * 2022/4/13
 */
public class AOPTransformer implements ClassFileTransformer {

    private final String className;

    public AOPTransformer(String className) {
        this.className = className;
    }

    /**
     * 注意这里的className是 a/b/C这样的而不是a.b.C
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className == null) {
            // 返回null表示不修改类字节码,和返回classfileBuffer是一样的效果。
            return null;
        }
        if (className.equals(this.className.replace('.', '/'))) {
            ClassPool classPool = ClassPool.getDefault();
            classPool.appendClassPath(new LoaderClassPath(loader));
            classPool.appendSystemPath();
            try {
                CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
                for (CtMethod declaredMethod : declaredMethods) {
                    declaredMethod.insertBefore("System.out.println(\"before invoke"+ declaredMethod.getName() + "\");");
                }
                return ctClass.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

然后通过mvn clean package进行打包,在target目录下可以得到一个fatjar(包含javassist等依赖),名为javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar

然后我们就可以通过-javaagent:/tmp/javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar=com.lzy.javaagent.Test 来使用agent了,注意-javaagent:后面要换成自己的agentjar包的绝对路径,=后面是传入的参数,我们这里的com.lzy.javaagent.Test是我们要aop的类。 如果是IDEA中使用,可以 img.png

例如我们编写一个简单的Test类

package com.lzy.javaagent;

/**
 * @author liuzhengyang
 * 2022/4/13
 */
public class Test {
    public void hello() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        new Test().hello();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在idea中添加先运行一次,然后修改Run Configuration,在vm options中添加-javaagent:/Users/liuzhengyang/Code/opensource/javaagent-example/target/javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar=com.lzy.javaagent.Test 运行,就可以看到AOP的效果了

com.lzy.javaagent.Test
before invokemain
before invokehello
hello
1
2
3
4

# 通过bytebuddy获取Instrumentation

有时修改-javaagent参数不是特别方便,比如使用方可能不方便或不知道怎么修改启动参数,有没有通过maven依赖代码调用的方式使用javaagent呢? 通过bytebuddy可以实现这一功能。

首先pom依赖中添加byte-buddy-agent的maven依赖

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.11.22</version>
</dependency>
1
2
3
4
5

然后通过ByteBuddyAgent.install(),就可以很方便的获得Instrumentation对象,接下来就可以添加ClassFileTransformer、调用redefine等等。

关于bytebuddy的使用和实现原理,我们会在后面文章中详细介绍。

public class TestByteBuddyInstall {
    public static void main(String[] args) {
        Instrumentation install = ByteBuddyAgent.install();
        System.out.println(install);
//        install.addTransformer();
    }
}
1
2
3
4
5
6
7

# Instrumentation接口介绍

我们对java.lang.instrument.Instrumentation类的重要方法进行一下介绍

方法 说明
void addTransformer(ClassFileTransformer transformer) 添加一个Transformer
void addTransformer(ClassFileTransformer transformer, boolean canRetransform) 添加一个Transformer,如果canRetransform为true这个transformer在类被retransform的时候会调用
void appendToBootstrapClassLoaderSearch(JarFile jarfile) 添加一个jar包让bootstrap classloader能够搜索到
void appendToSystemClassLoaderSearch(JarFile jarfile) 添加一个jar包让system classloader能够搜索到
Class[] getAllLoadedClasses() 获取当前所有已经加载的类
Class[] getInitiatedClasses(ClassLoader loader) 获取某个classloader已经初始化过的类
long getObjectSize(Object objectToSize) 获取某个对象的大小(不包含引用的传递大小,比如一个String字段,只计算这个字段的引用4byte)
void redefineClasses(ClassDefinition... definitions) 对某个类进行redefine修改代码,注意默认jdk只能修改方法体,不能进行增减字段方法等,dcevm jdk可以实现更强大的修改功能
boolean removeTransformer(ClassFileTransformer transformer) 从Instrumentation中删除Transformer
void retransformClasses(Class<?>... classes) 让一个已经加载的类重新transform,不过在retransform过程中和redefine一样,不能对类结构进行变更,只能修改方法体

# javaagent使用注意事项

  • javaagent的premain和agentmain的类是通过System ClassLoader(AppClassLoader)加载的,所以如果要和业务代码通信,需要考虑classloader不同的情况,一般要通过反射(可以传入指定classloader加载类)和业务代码通信。
  • 注意依赖冲突的问题,比如agent的fatjar中包含了某个第三方的类,业务代码中也包含了相同的第三方但是不同版本的类,由于classloader存在父类优先委派加载的情况,可能会导致类加载异常,所以一般会通过shaded修改第三方类库的包名或者通过classloader隔离

# 实现

# META-INF/MANIFEST.MF文件

javaagent在打包时,按照规范需要在jar包中的META-INF/MANIFEST.MF文件中声明javaagent的配置信息, 其中最关键的是Agent-Class、Premain-Class,这两个表示使用动态attach和-javaagent启动时调用的类, JVM会在这个类中寻找对应的agentmain和premain方法执行。 Can-Redefine-Classes、Can-Retransform-Classes表示此javaagent是否需要使用Instrumentation的 redefine和retransform的能力。 修改类的字节码有两个时机,一个javaagent通过Instrumentation.addTransformer方法注入ClassFileTransformer, 在类加载时,jvm会调用各个ClassFileTransformer,ClassFileTransformer可以修改类的字节码,但是如果要在类已经加载后再去修改它的字节码, 就需要使用redefine和retransform。

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3
Built-By: liuzhengyang
Build-Jdk: 11.0.11
Agent-Class: org.hotswap.agent.HotswapAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Implementation-Title: java-reload-agent-assembly
Implementation-Version: 1.0-SNAPSHOT
Premain-Class: org.hotswap.agent.HotswapAgent
Specification-Title: java-reload-agent-assembly
Specification-Version: 1.0-SNAPSHOT
1
2
3
4
5
6
7
8
9
10
11
12
13

# -javaagent: 执行流程

# 参数解析

例如当我们通过-javaagent:/Users/liuzhengyang/Code/opensource/java-reload-agent/java-reload-agent-assembly/target/java-reload-agent.jar 启动时,

以下代码位于jdk的arguments.cpp中,jvm解析传入的启动参数,对于-javaagent参数,会解析agent jar包路径和其他参数,并放到AgentLibraryList中。 AgentLibraryList是AgentLibrary的链表,AgentLibrary包含agent的名称参数等信息。

else if (match_option(option, "-javaagent:", &tail)) {
#if !INCLUDE_JVMTI
      jio_fprintf(defaultStream::error_stream(),
        "Instrumentation agents are not supported in this VM\n");
      return JNI_ERR;
#else
      if (tail != NULL) {
        size_t length = strlen(tail) + 1;
        char *options = NEW_C_HEAP_ARRAY(char, length, mtArguments);
        jio_snprintf(options, length, "%s", tail);
        add_instrument_agent("instrument", options, false);
        // java agents need module java.instrument
        if (!create_numbered_property("jdk.module.addmods", "java.instrument", addmods_count++)) {
          return JNI_ENOMEM;
        }
      }
#endif /

void Arguments::add_instrument_agent(const char* name, char* options, bool absolute_path) {
  _agentList.add(new AgentLibrary(name, options, absolute_path, NULL, true));
}

  // -agentlib and -agentpath arguments
  static AgentLibraryList _agentList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# agentLibrary加载使用

解析完启动参数后,jvm会创建vm,agentLibrary也是在这个过程中加载的。

create_vm方法判断Arguments::init_agents_at_startup()为true(AgentLibraryList不为空列表),则执行create_vm_init_agents

以下代码位于thread.cpp中。

jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
  extern void JDK_Version_init();

  // Preinitialize version info.
  VM_Version::early_initialize();

  // 省略其他代码...

  // Launch -agentlib/-agentpath and converted -Xrun agents
  if (Arguments::init_agents_at_startup()) {
    create_vm_init_agents();
  }

  // 省略其他代码...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

create_vm_init_agents方法负责初始化各个AgentLibrary,lookup_agent_on_load负责查找加载AgentLibrary对应的JVMTI动态链接库,然后调用对应JVMTI动态链接库的on_load_entry回调方法

void Threads::create_vm_init_agents() {
  extern struct JavaVM_ main_vm;
  AgentLibrary* agent;

  JvmtiExport::enter_onload_phase();

  for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
    // CDS dumping does not support native JVMTI agent.
    // CDS dumping supports Java agent if the AllowArchivingWithJavaAgent diagnostic option is specified.
    if (Arguments::is_dumping_archive()) {
      if(!agent->is_instrument_lib()) {
        vm_exit_during_cds_dumping("CDS dumping does not support native JVMTI agent, name", agent->name());
      } else if (!AllowArchivingWithJavaAgent) {
        vm_exit_during_cds_dumping(
          "Must enable AllowArchivingWithJavaAgent in order to run Java agent during CDS dumping");
      }
    }

    OnLoadEntry_t  on_load_entry = lookup_agent_on_load(agent);

    if (on_load_entry != NULL) {
      // Invoke the Agent_OnLoad function
      jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
      if (err != JNI_OK) {
        vm_exit_during_initialization("agent library failed to init", agent->name());
      }
    } else {
      vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());
    }
  }

  JvmtiExport::enter_primordial_phase();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

lookup_agent_on_load方法负责查找对应的jvmti动态链接库,对于javaagent,jvm中已经内置了对应的动态库名为instrument,位于jdk的lib文件夹下,比如mac下 是lib/libinstrument.dylib,linux中是lib/libinstrument.so。

// Find a command line agent library and return its entry point for
//         -agentlib:  -agentpath:   -Xrun
// num_symbol_entries must be passed-in since only the caller knows the number of symbols in the array.
static OnLoadEntry_t lookup_on_load(AgentLibrary* agent,
                                    const char *on_load_symbols[],
                                    size_t num_symbol_entries) {
  OnLoadEntry_t on_load_entry = NULL;
  void *library = NULL;

  if (!agent->valid()) {
    char buffer[JVM_MAXPATHLEN];
    char ebuf[1024] = "";
    const char *name = agent->name();
    const char *msg = "Could not find agent library ";

    // First check to see if agent is statically linked into executable
    if (os::find_builtin_agent(agent, on_load_symbols, num_symbol_entries)) {
      library = agent->os_lib();
    } else if (agent->is_absolute_path()) {
      library = os::dll_load(name, ebuf, sizeof ebuf);
      if (library == NULL) {
        const char *sub_msg = " in absolute path, with error: ";
        size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
        char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
        jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
        // If we can't find the agent, exit.
        vm_exit_during_initialization(buf, NULL);
        FREE_C_HEAP_ARRAY(char, buf);
      }
    } else {
      // Try to load the agent from the standard dll directory
      if (os::dll_locate_lib(buffer, sizeof(buffer), Arguments::get_dll_dir(),
                             name)) {
        library = os::dll_load(buffer, ebuf, sizeof ebuf);
      }
      if (library == NULL) { // Try the library path directory.
        if (os::dll_build_name(buffer, sizeof(buffer), name)) {
          library = os::dll_load(buffer, ebuf, sizeof ebuf);
        }
        if (library == NULL) {
          const char *sub_msg = " on the library path, with error: ";
          const char *sub_msg2 = "\nModule java.instrument may be missing from runtime image.";

          size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) +
                       strlen(ebuf) + strlen(sub_msg2) + 1;
          char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
          if (!agent->is_instrument_lib()) {
            jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
          } else {
            jio_snprintf(buf, len, "%s%s%s%s%s", msg, name, sub_msg, ebuf, sub_msg2);
          }
          // If we can't find the agent, exit.
          vm_exit_during_initialization(buf, NULL);
          FREE_C_HEAP_ARRAY(char, buf);
        }
      }
    }
    agent->set_os_lib(library);
    agent->set_valid();
  }

  // Find the OnLoad function.
  on_load_entry =
    CAST_TO_FN_PTR(OnLoadEntry_t, os::find_agent_function(agent,
                                                          false,
                                                          on_load_symbols,
                                                          num_symbol_entries));
  return on_load_entry;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

instrument动态链接库的实现位于java/instrumentat/share/native/libinstrument 入口为InvocationAdapter.c,on_load_entry方法实现是DEF_Agent_OnLoad方法。 createNewJPLISAgent是创建一个JPLISAgent(Java Programming Language Instrumentation Services) 创建完成JPLISAgent后,会读取保存premainClass、jarfile、bootClassPath等信息。

JNIEXPORT jint JNICALL
DEF_Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {
    JPLISInitializationError initerror  = JPLIS_INIT_ERROR_NONE;
    jint                     result     = JNI_OK;
    JPLISAgent *             agent      = NULL;

    initerror = createNewJPLISAgent(vm, &agent);
    if ( initerror == JPLIS_INIT_ERROR_NONE ) {
        int             oldLen, newLen;
        char *          jarfile;
        char *          options;
        jarAttribute*   attributes;
        char *          premainClass;
        char *          bootClassPath;

        /*
         * Parse <jarfile>[=options] into jarfile and options
         */
        if (parseArgumentTail(tail, &jarfile, &options) != 0) {
            fprintf(stderr, "-javaagent: memory allocation failure.\n");
            return JNI_ERR;
        }

        /*
         * Agent_OnLoad is specified to provide the agent options
         * argument tail in modified UTF8. However for 1.5.0 this is
         * actually in the platform encoding - see 5049313.
         *
         * Open zip/jar file and parse archive. If can't be opened or
         * not a zip file return error. Also if Premain-Class attribute
         * isn't present we return an error.
         */
        attributes = readAttributes(jarfile);
        if (attributes == NULL) {
            fprintf(stderr, "Error opening zip file or JAR manifest missing : %s\n", jarfile);
            free(jarfile);
            if (options != NULL) free(options);
            return JNI_ERR;
        }

        premainClass = getAttribute(attributes, "Premain-Class");
        if (premainClass == NULL) {
            fprintf(stderr, "Failed to find Premain-Class manifest attribute in %s\n",
                jarfile);
            free(jarfile);
            if (options != NULL) free(options);
            freeAttributes(attributes);
            return JNI_ERR;
        }

        /* Save the jarfile name */
        agent->mJarfile = jarfile;

        /*
         * The value of the Premain-Class attribute becomes the agent
         * class name. The manifest is in UTF8 so need to convert to
         * modified UTF8 (see JNI spec).
         */
        oldLen = (int)strlen(premainClass);
        newLen = modifiedUtf8LengthOfUtf8(premainClass, oldLen);
        if (newLen == oldLen) {
            premainClass = strdup(premainClass);
        } else {
            char* str = (char*)malloc( newLen+1 );
            if (str != NULL) {
                convertUtf8ToModifiedUtf8(premainClass, oldLen, str, newLen);
            }
            premainClass = str;
        }
        if (premainClass == NULL) {
            fprintf(stderr, "-javaagent: memory allocation failed\n");
            free(jarfile);
            if (options != NULL) free(options);
            freeAttributes(attributes);
            return JNI_ERR;
        }

        /*
         * If the Boot-Class-Path attribute is specified then we process
         * each relative URL and add it to the bootclasspath.
         */
        bootClassPath = getAttribute(attributes, "Boot-Class-Path");
        if (bootClassPath != NULL) {
            appendBootClassPath(agent, jarfile, bootClassPath);
        }

        /*
         * Convert JAR attributes into agent capabilities
         */
        convertCapabilityAttributes(attributes, agent);

        /*
         * Track (record) the agent class name and options data
         */
        initerror = recordCommandLineData(agent, premainClass, options);

        /*
         * Clean-up
         */
        if (options != NULL) free(options);
        freeAttributes(attributes);
        free(premainClass);
    }

    switch (initerror) {
    case JPLIS_INIT_ERROR_NONE:
      result = JNI_OK;
      break;
    case JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT:
      result = JNI_ERR;
      fprintf(stderr, "java.lang.instrument/-javaagent: cannot create native agent.\n");
      break;
    case JPLIS_INIT_ERROR_FAILURE:
      result = JNI_ERR;
      fprintf(stderr, "java.lang.instrument/-javaagent: initialization of native agent failed.\n");
      break;
    case JPLIS_INIT_ERROR_ALLOCATION_FAILURE:
      result = JNI_ERR;
      fprintf(stderr, "java.lang.instrument/-javaagent: allocation failure.\n");
      break;
    case JPLIS_INIT_ERROR_AGENT_CLASS_NOT_SPECIFIED:
      result = JNI_ERR;
      fprintf(stderr, "-javaagent: agent class not specified.\n");
      break;
    default:
      result = JNI_ERR;
      fprintf(stderr, "java.lang.instrument/-javaagent: unknown error\n");
      break;
    }
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

# 调用premain方法

在Thread::create_vm方法中,会调用post_vm_initialized,回调各个JVMTI动态链接库,其中instrument中

// Notify JVMTI agents that VM initialization is complete - nop if no agents.
  JvmtiExport::post_vm_initialized();
1
2

其中instrument的JVMTI入口在InvocationAdapter.c的eventHandlerVMInit方法,eventHandlerVMInit中会调用JPLISAgent的processJavaStart方法 来启动javaagent中的premain方法。

/*
 *  JVMTI callback support
 *
 *  We have two "stages" of callback support.
 *  At OnLoad time, we install a VMInit handler.
 *  When the VMInit handler runs, we remove the VMInit handler and install a
 *  ClassFileLoadHook handler.
 */

void JNICALL
eventHandlerVMInit( jvmtiEnv *      jvmtienv,
                    JNIEnv *        jnienv,
                    jthread         thread) {
    JPLISEnvironment * environment  = NULL;
    jboolean           success      = JNI_FALSE;

    environment = getJPLISEnvironment(jvmtienv);

    /* process the premain calls on the all the JPL agents */
    if (environment == NULL) {
        abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART ", getting JPLIS environment failed");
    }
    jthrowable outstandingException = NULL;
    /*
     * Add the jarfile to the system class path
     */
    JPLISAgent * agent = environment->mAgent;
    if (appendClassPath(agent, agent->mJarfile)) {
        fprintf(stderr, "Unable to add %s to system class path - "
                "the system class loader does not define the "
                "appendToClassPathForInstrumentation method or the method failed\n",
                agent->mJarfile);
        free((void *)agent->mJarfile);
        abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART ", appending to system class path failed");
    }
    free((void *)agent->mJarfile);
    agent->mJarfile = NULL;

    outstandingException = preserveThrowable(jnienv);
    success = processJavaStart( environment->mAgent, jnienv);
    restoreThrowable(jnienv, outstandingException);

    /* if we fail to start cleanly, bring down the JVM */
    if ( !success ) {
        abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART ", processJavaStart failed");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

processJavaStart负责调用agent jar包中的premain方法。 createInstrumentationImpl创建Instrumentation类的实例(sun.instrument.InstrumentationImpl) startJavaAgent会调用agent中的premain方法,传入Instrumentation类实例和agent参数。

/*
 * If this call fails, the JVM launch will ultimately be aborted,
 * so we don't have to be super-careful to clean up in partial failure
 * cases.
 */
jboolean
processJavaStart(   JPLISAgent *    agent,
                    JNIEnv *        jnienv) {
    jboolean    result;

    /*
     *  OK, Java is up now. We can start everything that needs Java.
     */

    /*
     *  First make our fallback InternalError throwable.
     */
    result = initializeFallbackError(jnienv);
    jplis_assert_msg(result, "fallback init failed");

    /*
     *  Now make the InstrumentationImpl instance.
     */
    if ( result ) {
        result = createInstrumentationImpl(jnienv, agent);
        jplis_assert_msg(result, "instrumentation instance creation failed");
    }


    /*
     *  Register a handler for ClassFileLoadHook (without enabling this event).
     *  Turn off the VMInit handler.
     */
    if ( result ) {
        result = setLivePhaseEventHandlers(agent);
        jplis_assert_msg(result, "setting of live phase VM handlers failed");
    }

    /*
     *  Load the Java agent, and call the premain.
     */
    if ( result ) {
        result = startJavaAgent(agent, jnienv,
                                agent->mAgentClassName, agent->mOptionsString,
                                agent->mPremainCaller);
        jplis_assert_msg(result, "agent load/premain call failed");
    }

    /*
     * Finally surrender all of the tracking data that we don't need any more.
     * If something is wrong, skip it, we will be aborting the JVM anyway.
     */
    if ( result ) {
        deallocateCommandLineData(agent);
    }

    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# Can-Redefine-Classes和Can-Retransform-Classes的作用

jvmti (opens new window)

# 运行时attach加载agent

在启动时通过javaagent加载agent在一些情况下不太方便,比如有时候我们想对运行中的程序进行一些类的变更, 比如进行性能分析或者程序诊断,如果要修改启动参数重启,可能会导致现场被破坏,修改参数重启也不是很方便,这时jdk提供的动态attach加载agent功能就非常方便了。 arthas和jprofiler均能这种方式。

attach和loadAgent代码实例如下,首先通过VirtualMachine.attach attach到本机的某个java进程, 得到VirtualMachine, 然后调用VirtualMachine的loadAgent方法加载调用具体的路径的javaagent jar包。

这个是由jdk的AttachListener实现的,除了attach后加载javaagent,jdk中的jstack,jcmd等命令也都是使用AttachListener机制和jvm通信的。

String pid = "要attach的目标进程id";
String agentPath = "javaagent jar包的绝对路径";
String agentOptions = "可选的传给agentmain方法的参数";
try {
    VirtualMachine virtualMachine = VirtualMachine.attach(pid);
    virtualMachine.loadAgent(agentPath, agentOptions);
    virtualMachine.detach();
} catch (Exception e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10

# attach客户端

jvm在tmpdir目录下(linux下是/tmp)创建.java_pid<pid>文件(<pid>是进程id)用来和客户端通信, 默认情况下不会提前创建,客户端会通过向目标java进程发送QUIT信号,java进程收到QUIT后会创建这个通信文件。

VirtualMachineImpl(AttachProvider provider, String vmid)
        throws AttachNotSupportedException, IOException
{
    super(provider, vmid);

    int pid;
    try {
        pid = Integer.parseInt(vmid);
    } catch (NumberFormatException x) {
        throw new AttachNotSupportedException("Invalid process identifier");
    }

    // Find the socket file. If not found then we attempt to start the
    // attach mechanism in the target VM by sending it a QUIT signal.
    // Then we attempt to find the socket file again.
    File socket_file = new File(tmpdir, ".java_pid" + pid);
    socket_path = socket_file.getPath();
    if (!socket_file.exists()) {
        File f = createAttachFile(pid);
        sendQuitTo(pid);
    // ...

    int s = socket();
    try {
        connect(s, socket_path);
    } finally {
        close(s);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

创建完VirtualMachine以及socket通信后,就可以向jvm发送消息了。 loadAgent调用loadAgentLibrary传入instrument表示使用这个JVMTI动态链接库,并且传入args参数。

public void loadAgent(String agent, String options)
        throws AgentLoadException, AgentInitializationException, IOException
{
    // ...
    String args = agent;
    if (options != null) {
        args = args + "=" + options;
    }
    try {
        loadAgentLibrary("instrument", args);
    } catch (AgentInitializationException x) {
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

loadAgentLibrary

/*
private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
throws AgentLoadException, AgentInitializationException, IOException
{
InputStream in = execute("load", agentLibrary, isAbsolute ? "true" : "false", options);
// ...
}
1
2
3
4
5
6
7

execute负责通过.java_pid<pid>这个socket文件和jvm进行通信发送cmd和相关参数。

InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
        int s = socket();

        // connect to target VM
        try {
            connect(s, socket_path);
        } catch (IOException x) {
            close(s);
            throw x;
        }

        try {
            writeString(s, PROTOCOL_VERSION);
            writeString(s, cmd);

            for (int i=0; i<3; i++) {
                if (i < args.length && args[i] != null) {
                    writeString(s, (String)args[i]);
                } else {
                    writeString(s, "");
                }
            }
        // ...
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# AttachListener

AttachListener提供jvm外部和jvm通信的通道。

AttachListener初始化时默认不启动(降低资源消耗),Attach客户端会先判断是否有.java_pid<pid>文件,如果没有 向java进程发送QUIT信号,jvm监听这个信号,如果没有启动AttachListener则会进行AttachListener创建初始化

os.cpp中的signal_thread_entry方法
switch (sig) {
      case SIGBREAK: {
        if (!DisableAttachMechanism) {
          AttachListenerState cur_state = AttachListener::transit_state(AL_INITIALIZING, AL_NOT_INITIALIZED);
          if (cur_state == AL_INITIALIZING) {
            continue;
          } else if (cur_state == AL_NOT_INITIALIZED) {
            if (AttachListener::is_init_trigger()) {
              continue;
}

void AttachListener::init() {
  const char thread_name[] = "Attach Listener";
  Handle string = java_lang_String::create_from_str(thread_name, THREAD);
  if (has_init_error(THREAD)) {
    set_state(AL_NOT_INITIALIZED);
    return;
  }

  Handle thread_group (THREAD, Universe::system_thread_group());
  Handle thread_oop = JavaCalls::construct_new_instance(SystemDictionary::Thread_klass(),
                       vmSymbols::threadgroup_string_void_signature(),
                       thread_group,
                       string,
                       THREAD);
  if (has_init_error(THREAD)) {
    set_state(AL_NOT_INITIALIZED);
    return;
  }

  Klass* group = SystemDictionary::ThreadGroup_klass();
  JavaValue result(T_VOID);
  JavaCalls::call_special(&result,
                        thread_group,
                        group,
                        vmSymbols::add_method_name(),
                        vmSymbols::thread_void_signature(),
                        thread_oop,
                        THREAD);
  if (has_init_error(THREAD)) {
    set_state(AL_NOT_INITIALIZED);
    return;
  }

  { MutexLocker mu(Threads_lock);
    JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry);

    // Check that thread and osthread were created
    if (listener_thread == NULL || listener_thread->osthread() == NULL) {
      vm_exit_during_initialization("java.lang.OutOfMemoryError",
                                    os::native_thread_creation_failed_msg());
    }

    java_lang_Thread::set_thread(thread_oop(), listener_thread);
    java_lang_Thread::set_daemon(thread_oop());

    listener_thread->set_threadObj(thread_oop());
    Threads::add(listener_thread);
    Thread::start(listener_thread);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

其中不同类型的交互抽象成了AttachOperation,目前已经支持的 operation如下。

// names must be of length <= AttachOperation::name_length_max
static AttachOperationFunctionInfo funcs[] = {
  { "agentProperties",  get_agent_properties },
  { "datadump",         data_dump },
  { "dumpheap",         dump_heap },
  { "load",             load_agent },
  { "properties",       get_system_properties },
  { "threaddump",       thread_dump },
  { "inspectheap",      heap_inspection },
  { "setflag",          set_flag },
  { "printflag",        print_flag },
  { "jcmd",             jcmd },
  { NULL,               NULL }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

调用VirtualMachine.load方法会发送一个load类型的AttachOperation,对应的处理函数是load_agent

// Implementation of "load" command.
static jint load_agent(AttachOperation* op, outputStream* out) {
  // get agent name and options
  const char* agent = op->arg(0);
  const char* absParam = op->arg(1);
  const char* options = op->arg(2);

  // If loading a java agent then need to ensure that the java.instrument module is loaded
  if (strcmp(agent, "instrument") == 0) {
    Thread* THREAD = Thread::current();
    ResourceMark rm(THREAD);
    HandleMark hm(THREAD);
    JavaValue result(T_OBJECT);
    Handle h_module_name = java_lang_String::create_from_str("java.instrument", THREAD);
    JavaCalls::call_static(&amp;result,
                           SystemDictionary::module_Modules_klass(),
                           vmSymbols::loadModule_name(),
                           vmSymbols::loadModule_signature(),
                           h_module_name,
                           THREAD);
    if (HAS_PENDING_EXCEPTION) {
      java_lang_Throwable::print(PENDING_EXCEPTION, out);
      CLEAR_PENDING_EXCEPTION;
      return JNI_ERR;
    }
  }

  return JvmtiExport::load_agent_library(agent, absParam, options, out);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# ClassFileTransformer是如何注册、调用的

# ClassFileTransformer注册

Instrumentation.addTransformer会将Transformer保存到TransformerManager类中,按照能否retransform分为两个TransformerManager,每个TransformerManager中通过数组保存Transformer。

public synchronized void
addTransformer(ClassFileTransformer transformer, boolean canRetransform) {
    if (transformer == null) {
        throw new NullPointerException("null passed as 'transformer' in addTransformer");
    }
    if (canRetransform) {
        if (!isRetransformClassesSupported()) {
            throw new UnsupportedOperationException(
              "adding retransformable transformers is not supported in this environment");
        }
        if (mRetransfomableTransformerManager == null) {
            mRetransfomableTransformerManager = new TransformerManager(true);
        }
        mRetransfomableTransformerManager.addTransformer(transformer);
        if (mRetransfomableTransformerManager.getTransformerCount() == 1) {
            setHasRetransformableTransformers(mNativeAgent, true);
        }
    } else {
        mTransformerManager.addTransformer(transformer);
        if (mTransformerManager.getTransformerCount() == 1) {
            setHasTransformers(mNativeAgent, true);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public synchronized void
addTransformer( ClassFileTransformer    transformer) {
    TransformerInfo[] oldList = mTransformerList;
    TransformerInfo[] newList = new TransformerInfo[oldList.length + 1];
    System.arraycopy(   oldList,
                        0,
                        newList,
                        0,
                        oldList.length);
    newList[oldList.length] = new TransformerInfo(transformer);
    mTransformerList = newList;
}
1
2
3
4
5
6
7
8
9
10
11
12

# ClassFileTransformer调用

那么ClassFileTransformer是如何被调用的呢,以类加载时调用ClassFileTransformer为例。

在jvm加载类时,会回调各个jvmti调用类加载事件回调接口ClassFileLoadHook。其中jvm把能够retransform和不能retramform分为两个jvmtiEnv环境, 分别调用这两个jvmti的回调方法,所以对于类加载时的transform,通过addTransformer无论参数canRetransform是否为true都能调用到。

img.png

instrument jvmti的ClassFileLoadHook实现是调用InstrumentationImpl的transform方法。

void
transformClassFile(             JPLISAgent *            agent,
                                JNIEnv *                jnienv,
                                jobject                 loaderObject,
                                const char*             name,
                                jclass                  classBeingRedefined,
                                jobject                 protectionDomain,
                                jint                    class_data_len,
                                const unsigned char*    class_data,
                                jint*                   new_class_data_len,
                                unsigned char**         new_class_data,
                                jboolean                is_retransformer) {
    // ...省略
            transformedBufferObject = (*jnienv)->CallObjectMethod(
                                                jnienv,
                                                agent->mInstrumentationImpl,
                                                agent->mTransform,
                                                moduleObject,
                                                loaderObject,
                                                classNameStringObject,
                                                classBeingRedefined,
                                                protectionDomain,
                                                classFileBufferObject,
                                                is_retransformer);
            errorOutstanding = checkForAndClearThrowable(jnienv);
            jplis_assert_msg(!errorOutstanding, "transform method call failed");
        }

        if ( !errorOutstanding ) {
            *new_class_data_len = (transformedBufferSize);
            *new_class_data     = resultBuffer;
        }

        // ...省略
    }
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

InstrumentationImpl的transform方法的实现是根据当前是否是retransform来选择TransformerManager,然后调用TransformerManager的transform方法。

// WARNING: the native code knows the name & signature of this method
    private byte[]
    transform(  Module              module,
                ClassLoader         loader,
                String              classname,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer,
                boolean             isRetransformer) {
        TransformerManager mgr = isRetransformer?
                                        mRetransfomableTransformerManager :
                                        mTransformerManager;
        // module is null when not a class load or when loading a class in an
        // unnamed module and this is the first type to be loaded in the package.
        if (module == null) {
            if (classBeingRedefined != null) {
                module = classBeingRedefined.getModule();
            } else {
                module = (loader == null) ? jdk.internal.loader.BootLoader.getUnnamedModule()
                                          : loader.getUnnamedModule();
            }
        }
        if (mgr == null) {
            return null; // no manager, no transform
        } else {
            return mgr.transform(   module,
                                    loader,
                                    classname,
                                    classBeingRedefined,
                                    protectionDomain,
                                    classfileBuffer);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

TransformerManager的transform方法实现逻辑是依次调用Transformer数组中的各个Transformer(就像server中的Filter),然后把最终的bytes结果返回。

public byte[]
    transform(  Module              module,
                ClassLoader         loader,
                String              classname,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer) {
        boolean someoneTouchedTheBytecode = false;

        TransformerInfo[]  transformerList = getSnapshotTransformerList();

        byte[]  bufferToUse = classfileBuffer;

        // order matters, gotta run 'em in the order they were added
        for ( int x = 0; x < transformerList.length; x++ ) {
            TransformerInfo         transformerInfo = transformerList[x];
            ClassFileTransformer    transformer = transformerInfo.transformer();
            byte[]                  transformedBytes = null;

            try {
                transformedBytes = transformer.transform(   module,
                                                            loader,
                                                            classname,
                                                            classBeingRedefined,
                                                            protectionDomain,
                                                            bufferToUse);
            }
            catch (Throwable t) {
                // don't let any one transformer mess it up for the others.
                // This is where we need to put some logging. What should go here? FIXME
            }

            if ( transformedBytes != null ) {
                someoneTouchedTheBytecode = true;
                bufferToUse = transformedBytes;
            }
        }

        // if someone modified it, return the modified buffer.
        // otherwise return null to mean "no transforms occurred"
        byte [] result;
        if ( someoneTouchedTheBytecode ) {
            result = bufferToUse;
        }
        else {
            result = null;
        }

        return result;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# redefine的实现

jvmtiEnv.cpp中定义了RedefineClasses方法的入口,会委托给VM_RedefineClasses执行。 VM_RedefineClasses是jvm中定义的一种VM_Operation实现, VMThread::execute会判断VM_Operation是否需要在safepoint中执行, 如果需要会进入safepoint,然后调用对应VM_Operation的evaluate方法,VM_Operation的evaluate会调用doit方法

// 位于jvmtiEnv.cpp中
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
  VMThread::execute(&op);
  return (op.check_error());
} /* end RedefineClasses */
1
2
3
4
5
6
7

VM_RedefineClasses代码位于jvmtiRedefineClasses.cpp

doit方法的逻辑是

  • 标记正在栈上执行的方法,栈上执行的方法还会执行旧的代码。
  • 对每个类进行redefine
  • 清理JIT的compiled code,范围是对redefined的类有依赖的代码
  • 调整JvmtiExport的increment_redefinition_count
  • 清理redefine类旧的metadata
void VM_RedefineClasses::doit() {
  Thread *thread = Thread::current();
  // ... 标记正在栈上执行的方法,栈上执行的方法还会执行旧的代码。
  // Mark methods seen on stack and everywhere else so old methods are not
  // cleaned up if they're on the stack.
  MetadataOnStackMark md_on_stack(/*walk_all_metadata*/true, /*redefinition_walk*/true);
  HandleMark hm(thread);   // make sure any handles created are deleted
                           // before the stack walk again.

  for (int i = 0; i < _class_count; i++) {
    redefine_single_class(_class_defs[i].klass, _scratch_classes[i], thread);
  }

  // 清理JIT的compiled code
  // Flush all compiled code that depends on the classes redefined.
  flush_dependent_code();

  // 调整常量池缓存和指向变更的类的方法的类的vtable
  // Adjust constantpool caches and vtables for all classes
  // that reference methods of the evolved classes.
  // Have to do this after all classes are redefined and all methods that
  // are redefined are marked as old.
  AdjustAndCleanMetadata adjust_and_clean_metadata(thread);
  ClassLoaderDataGraph::classes_do(&adjust_and_clean_metadata);

  // JSR-292 support
  if (_any_class_has_resolved_methods) {
    bool trace_name_printed = false;
    ResolvedMethodTable::adjust_method_entries(&trace_name_printed);
  }

  // 增加classRedefinedCount,在java.lang.Class中获取反射数据会判断这个值,发现变化后会重新读取类的反射数据。
  // Increment flag indicating that some invariants are no longer true.
  // See jvmtiExport.hpp for detailed explanation.
  JvmtiExport::increment_redefinition_count();

  // 清理redefine类旧的metadata
  // Clean up any metadata now unreferenced while MetadataOnStackMark is set.
  ClassLoaderDataGraph::clean_deallocate_lists(false);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

redefine_single_class是更新每个单独的calss

  • 清理断点和缓存,对依赖的JIT compiled code进行去优化(deoptimization)
  • 调整常量池缓存和指向这个类的方法的类的vtable
  • 增加classRedefinedCount,在java.lang.Class中获取反射数据会判断这个值,发现变化后会重新读取类的反射数据。
void VM_RedefineClasses::redefine_single_class(jclass the_jclass,
       InstanceKlass* scratch_class, TRAPS) {

  HandleMark hm(THREAD);   // make sure handles from this call are freed

  if (log_is_enabled(Info, redefine, class, timer)) {
    _timer_rsc_phase1.start();
  }

  InstanceKlass* the_class = get_ik(the_jclass);

  // Set some flags to control and optimize adjusting method entries
  _has_redefined_Object |= the_class == SystemDictionary::Object_klass();
  _has_null_class_loader |= the_class->class_loader() == NULL;

  // Remove all breakpoints in methods of this class
  JvmtiBreakpoints& jvmti_breakpoints = JvmtiCurrentBreakpoints::get_jvmti_breakpoints();
  jvmti_breakpoints.clearall_in_class_at_safepoint(the_class);

  // Mark all compiled code that depends on this class
  mark_dependent_code(the_class);

  _old_methods = the_class->methods();
  _new_methods = scratch_class->methods();
  _the_class = the_class;
  compute_added_deleted_matching_methods();
  update_jmethod_ids(THREAD);

  _any_class_has_resolved_methods = the_class->has_resolved_methods() || _any_class_has_resolved_methods;

  // Attach new constant pool to the original klass. The original
  // klass still refers to the old constant pool (for now).
  scratch_class->constants()->set_pool_holder(the_class);

#if 0
  // In theory, with constant pool merging in place we should be able
  // to save space by using the new, merged constant pool in place of
  // the old constant pool(s). By "pool(s)" I mean the constant pool in
  // the klass version we are replacing now and any constant pool(s) in
  // previous versions of klass. Nice theory, doesn't work in practice.
  // When this code is enabled, even simple programs throw NullPointer
  // exceptions. I'm guessing that this is caused by some constant pool
  // cache difference between the new, merged constant pool and the
  // constant pool that was just being used by the klass. I'm keeping
  // this code around to archive the idea, but the code has to remain
  // disabled for now.

  // Attach each old method to the new constant pool. This can be
  // done here since we are past the bytecode verification and
  // constant pool optimization phases.
  for (int i = _old_methods->length() - 1; i >= 0; i--) {
    Method* method = _old_methods->at(i);
    method->set_constants(scratch_class->constants());
  }

  // NOTE: this doesn't work because you can redefine the same class in two
  // threads, each getting their own constant pool data appended to the
  // original constant pool.  In order for the new methods to work when they
  // become old methods, they need to keep their updated copy of the constant pool.

  {
    // walk all previous versions of the klass
    InstanceKlass *ik = the_class;
    PreviousVersionWalker pvw(ik);
    do {
      ik = pvw.next_previous_version();
      if (ik != NULL) {

        // attach previous version of klass to the new constant pool
        ik->set_constants(scratch_class->constants());

        // Attach each method in the previous version of klass to the
        // new constant pool
        Array<Method*>* prev_methods = ik->methods();
        for (int i = prev_methods->length() - 1; i >= 0; i--) {
          Method* method = prev_methods->at(i);
          method->set_constants(scratch_class->constants());
        }
      }
    } while (ik != NULL);
  }
#endif

  // Replace methods and constantpool
  the_class->set_methods(_new_methods);
  scratch_class->set_methods(_old_methods);     // To prevent potential GCing of the old methods,
                                          // and to be able to undo operation easily.

  Array<int>* old_ordering = the_class->method_ordering();
  the_class->set_method_ordering(scratch_class->method_ordering());
  scratch_class->set_method_ordering(old_ordering);

  ConstantPool* old_constants = the_class->constants();
  the_class->set_constants(scratch_class->constants());
  scratch_class->set_constants(old_constants);  // See the previous comment.
#if 0
  // We are swapping the guts of "the new class" with the guts of "the
  // class". Since the old constant pool has just been attached to "the
  // new class", it seems logical to set the pool holder in the old
  // constant pool also. However, doing this will change the observable
  // class hierarchy for any old methods that are still executing. A
  // method can query the identity of its "holder" and this query uses
  // the method's constant pool link to find the holder. The change in
  // holding class from "the class" to "the new class" can confuse
  // things.
  //
  // Setting the old constant pool's holder will also cause
  // verification done during vtable initialization below to fail.
  // During vtable initialization, the vtable's class is verified to be
  // a subtype of the method's holder. The vtable's class is "the
  // class" and the method's holder is gotten from the constant pool
  // link in the method itself. For "the class"'s directly implemented
  // methods, the method holder is "the class" itself (as gotten from
  // the new constant pool). The check works fine in this case. The
  // check also works fine for methods inherited from super classes.
  //
  // Miranda methods are a little more complicated. A miranda method is
  // provided by an interface when the class implementing the interface
  // does not provide its own method.  These interfaces are implemented
  // internally as an InstanceKlass. These special instanceKlasses
  // share the constant pool of the class that "implements" the
  // interface. By sharing the constant pool, the method holder of a
  // miranda method is the class that "implements" the interface. In a
  // non-redefine situation, the subtype check works fine. However, if
  // the old constant pool's pool holder is modified, then the check
  // fails because there is no class hierarchy relationship between the
  // vtable's class and "the new class".

  old_constants->set_pool_holder(scratch_class());
#endif

  // track number of methods that are EMCP for add_previous_version() call below
  int emcp_method_count = check_methods_and_mark_as_obsolete();
  transfer_old_native_function_registrations(the_class);

  // The class file bytes from before any retransformable agents mucked
  // with them was cached on the scratch class, move to the_class.
  // Note: we still want to do this if nothing needed caching since it
  // should get cleared in the_class too.
  if (the_class->get_cached_class_file() == 0) {
    // the_class doesn't have a cache yet so copy it
    the_class->set_cached_class_file(scratch_class->get_cached_class_file());
  }
  else if (scratch_class->get_cached_class_file() !=
           the_class->get_cached_class_file()) {
    // The same class can be present twice in the scratch classes list or there
    // are multiple concurrent RetransformClasses calls on different threads.
    // In such cases we have to deallocate scratch_class cached_class_file.
    os::free(scratch_class->get_cached_class_file());
  }

  // NULL out in scratch class to not delete twice.  The class to be redefined
  // always owns these bytes.
  scratch_class->set_cached_class_file(NULL);

  // Replace inner_classes
  Array<u2>* old_inner_classes = the_class->inner_classes();
  the_class->set_inner_classes(scratch_class->inner_classes());
  scratch_class->set_inner_classes(old_inner_classes);

  // Initialize the vtable and interface table after
  // methods have been rewritten
  // no exception should happen here since we explicitly
  // do not check loader constraints.
  // compare_and_normalize_class_versions has already checked:
  //  - classloaders unchanged, signatures unchanged
  //  - all instanceKlasses for redefined classes reused & contents updated
  the_class->vtable().initialize_vtable(false, THREAD);
  the_class->itable().initialize_itable(false, THREAD);
  assert(!HAS_PENDING_EXCEPTION || (THREAD->pending_exception()->is_a(SystemDictionary::ThreadDeath_klass())), "redefine exception");

  // Leave arrays of jmethodIDs and itable index cache unchanged

  // Copy the "source file name" attribute from new class version
  the_class->set_source_file_name_index(
    scratch_class->source_file_name_index());

  // Copy the "source debug extension" attribute from new class version
  the_class->set_source_debug_extension(
    scratch_class->source_debug_extension(),
    scratch_class->source_debug_extension() == NULL ? 0 :
    (int)strlen(scratch_class->source_debug_extension()));

  // Use of javac -g could be different in the old and the new
  if (scratch_class->access_flags().has_localvariable_table() !=
      the_class->access_flags().has_localvariable_table()) {

    AccessFlags flags = the_class->access_flags();
    if (scratch_class->access_flags().has_localvariable_table()) {
      flags.set_has_localvariable_table();
    } else {
      flags.clear_has_localvariable_table();
    }
    the_class->set_access_flags(flags);
  }

  swap_annotations(the_class, scratch_class);

  // Replace minor version number of class file
  u2 old_minor_version = the_class->minor_version();
  the_class->set_minor_version(scratch_class->minor_version());
  scratch_class->set_minor_version(old_minor_version);

  // Replace major version number of class file
  u2 old_major_version = the_class->major_version();
  the_class->set_major_version(scratch_class->major_version());
  scratch_class->set_major_version(old_major_version);

  // Replace CP indexes for class and name+type of enclosing method
  u2 old_class_idx  = the_class->enclosing_method_class_index();
  u2 old_method_idx = the_class->enclosing_method_method_index();
  the_class->set_enclosing_method_indices(
    scratch_class->enclosing_method_class_index(),
    scratch_class->enclosing_method_method_index());
  scratch_class->set_enclosing_method_indices(old_class_idx, old_method_idx);

  // Replace fingerprint data
  the_class->set_has_passed_fingerprint_check(scratch_class->has_passed_fingerprint_check());
  the_class->store_fingerprint(scratch_class->get_stored_fingerprint());

  the_class->set_has_been_redefined();

  if (!the_class->should_be_initialized()) {
    // Class was already initialized, so AOT has only seen the original version.
    // We need to let AOT look at it again.
    AOTLoader::load_for_klass(the_class, THREAD);
  }

  // keep track of previous versions of this class
  the_class->add_previous_version(scratch_class, emcp_method_count);

  _timer_rsc_phase1.stop();
  if (log_is_enabled(Info, redefine, class, timer)) {
    _timer_rsc_phase2.start();
  }

  if (the_class->oop_map_cache() != NULL) {
    // Flush references to any obsolete methods from the oop map cache
    // so that obsolete methods are not pinned.
    the_class->oop_map_cache()->flush_obsolete_entries();
  }

  increment_class_counter((InstanceKlass *)the_class, THREAD);
  {
    ResourceMark rm(THREAD);
    // increment the classRedefinedCount field in the_class and in any
    // direct and indirect subclasses of the_class
    log_info(redefine, class, load)
      ("redefined name=%s, count=%d (avail_mem=" UINT64_FORMAT "K)",
       the_class->external_name(), java_lang_Class::classRedefinedCount(the_class->java_mirror()), os::available_memory() >> 10);
    Events::log_redefinition(THREAD, "redefined class name=%s, count=%d",
                             the_class->external_name(),
                             java_lang_Class::classRedefinedCount(the_class->java_mirror()));

  }
  _timer_rsc_phase2.stop();
} // end redefine_single_class()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258

# retransform的实现

retransform可以理解为获取要transform的类的字节码,通过所有的canRetransform的ClassFileTramsformer 进行transform,最终得到transform后的字节码,然后调用redefine修改字节码。所以retransform的修改限制比如不能增减字段方法等和redefine是一致的。

// 代码位于jvmtiEnv.cpp中
JvmtiEnv::RetransformClasses(jint class_count, const jclass* classes) {
  int index;
  JavaThread* current_thread = JavaThread::current();
  ResourceMark rm(current_thread);

  jvmtiClassDefinition* class_definitions =
                            NEW_RESOURCE_ARRAY(jvmtiClassDefinition, class_count);
  NULL_CHECK(class_definitions, JVMTI_ERROR_OUT_OF_MEMORY);

  for (index = 0; index < class_count; index++) {
    HandleMark hm(current_thread);

    jclass jcls = classes[index];
    oop k_mirror = JNIHandles::resolve_external_guard(jcls);
    if (k_mirror == NULL) {
      return JVMTI_ERROR_INVALID_CLASS;
    }
    if (!k_mirror->is_a(SystemDictionary::Class_klass())) {
      return JVMTI_ERROR_INVALID_CLASS;
    }

    if (!VM_RedefineClasses::is_modifiable_class(k_mirror)) {
      return JVMTI_ERROR_UNMODIFIABLE_CLASS;
    }

    Klass* klass = java_lang_Class::as_Klass(k_mirror);

    jint status = klass->jvmti_class_status();
    if (status & (JVMTI_CLASS_STATUS_ERROR)) {
      return JVMTI_ERROR_INVALID_CLASS;
    }

    InstanceKlass* ik = InstanceKlass::cast(klass);
    if (ik->get_cached_class_file_bytes() == NULL) {
      // Not cached, we need to reconstitute the class file from the
      // VM representation. We don't attach the reconstituted class
      // bytes to the InstanceKlass here because they have not been
      // validated and we're not at a safepoint.
      JvmtiClassFileReconstituter reconstituter(ik);
      if (reconstituter.get_error() != JVMTI_ERROR_NONE) {
        return reconstituter.get_error();
      }

      class_definitions[index].class_byte_count = (jint)reconstituter.class_file_size();
      class_definitions[index].class_bytes      = (unsigned char*)
                                                       reconstituter.class_file_bytes();
    } else {
      // it is cached, get it from the cache
      class_definitions[index].class_byte_count = ik->get_cached_class_file_len();
      class_definitions[index].class_bytes      = ik->get_cached_class_file_bytes();
    }
    class_definitions[index].klass              = jcls;
  }
  // 通过transform后,得到最新的class_definitions,再调用RedefineClasses修改类的字节码
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_retransform);
  VMThread::execute(&op);
  return (op.check_error());
} /* end RetransformClasses */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 注意事项

在开发javaagent程序时,如果使用attach后load agent的方式,目前jdk有一个问题时,load一个agent之后,jvm有agent的类缓存机制, 之后即使再更新javaagent文件,javaagent的类也不会更新,所以开发时如果发现效果和代码不一致,很有可能是这个原因导致。一个简单的解决方式是重启 被attach的进程。

# 总结

本文我们掌握了javaagent的常见应用场景比如分布式tracing、性能分析、在线诊断、热更新等。 了解了如何创建一个javaagent来实现AOP功能以及如何使用它。 了解了javaagent在启动时加载和运行时加载的两种使用方式,还有通过ByteBuddyAgent.install()的使用方式。 了解了VirtualMachine.attach()以及loadAgent是如何通过Attach Listener与jvm通信的。 了解了jvm中的instrument动态链接库的实现。