javaagent 介绍、使用、实现详解 - 今日头条

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

通过字节码修改,可以实现监控 tracing、性能分析、在线诊断、代码热更新热部署等等各种能力。监控 tracing: 分布式 tracing 框架的 J

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 的方式采集方法的执行栈,instrumentatino 就是修改字节码来收集方法的执行次数、耗时等信息。
  • 在线诊断: arthas 这样的软件使用 javaagent 技术在运行时将诊断逻辑注入到已有代码中,实现 watch,trace 等功能
  • 代码热更新、热部署: 通过 javaagent 技术,还能够实现 Java 代码的热更新,减少 Java 服务重启次数,提升开发效率,比如开源的 https://github.com/HotswapProjects/HotswapAgenthttps://github.com/dcevm/dcevm

我们以 javaagent-example 项目为例使用字节码实现一个最简单的 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)

 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
<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>

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

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

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

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

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

最后编写 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 目标类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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));
	}
}

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

 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
/**
 * @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;
    }
}

然后通过 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 中使用,可以

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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();
    }
}

在 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 的效果了

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

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

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

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

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

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

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

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

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。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

例如当我们通过 -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 的名称参数等信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;

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

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

以下代码位于 thread.cpp 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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();
  }

  // 省略其他代码...
}

create_vm_init_agents 方法负责初始化各个 AgentLibrary,lookup_agent_on_load 负责查找加载 AgentLibrary 对应的 JVMTI 动态链接库,然后调用对应 JVMTI 动态链接库的 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
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();
}

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

 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
// 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;
}

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 等信息。

  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
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;
}

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

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

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

 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
/*
 *  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");
    }
}

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

 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
/*
 * 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;
}

jvmti

在启动时通过 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 通信的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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();
}

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

 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
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);
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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) {
    // ...
}

loadAgentLibrary

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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, "");
                }
            }
        // ...
    }

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

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

 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
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);
  }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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 }
};

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

 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
// 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(&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);
}

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

 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, 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
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;
}

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

在 jvm 加载类时,会回调各个 jvmti 调用类加载事件回调接口 ClassFileLoadHook

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

 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
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;
}

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

 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
// 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);
        }
    }

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

 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
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;
    }

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