javaagent介绍、使用、实现详解

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

使用

编写、打包、使用javaagent

我们以[javaagent-example](https://github.com/liuzhengyang/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)

<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

<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能力。

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目标类

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,在加载指定的类时,对类进行修改在方法调用前增加代码,打印方法名。

/**
 * @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类

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

com.lzy.javaagent.Test
before invokemain
before invokehello
hello

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

然后通过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();
    }
}

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

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

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

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

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

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

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

调用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();

其中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");
    }
}

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

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

jvmti

运行时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();
}

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

创建完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) {
    // ...
}

loadAgentLibrary

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

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

其中不同类型的交互抽象成了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 }
};

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

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);
        }
    }
}
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是如何被调用的呢,以类加载时调用ClassFileTransformer为例。

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

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

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

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

总结

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

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注