Skip to content

Java Agent原理解析

在遇到性能问题时经常使用的诊断工具如arthas和btrace都是基于Java Agent实现的。Java Agent 是一个Jar包,启动方式和普通Jar包不同,对于普通的Jar包,通过指定类的main函数进行启动,但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行。本章先实现一个简单的Java Agent,然后对Java Agent的初始化和底层实现源码做分析。

6.1 Java Agent 基础

6.1.1 实现一个简单的Java Agent

Agent类的代码如下:

java
package org.example;
import java.lang.instrument.Instrumentation;

public class Agent {
    // 以vm参数的方式载入,在Java程序的main方法执行之前执行
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain run");
    }
    // 以Attach的方式载入,在Java程序启动后执行
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain run");
    }
}

需要在agentmain或者premain方法中实现具体的Agent逻辑,如读取线程状态、监控数据和修改类的字节码等。

因为Java Agent的特殊性,还需要一些特殊的配置,在META-INF目录下创建MANIFEST.MF文件,这部分可以手动生成也可以使用maven插件自动生成,这里建议使用maven插件自动生成。在pom.xml文件中添加如下插件配置,其中Premain-ClassAgent-Class的配置值为上面Agent类的全限定名称。配置如下:

java
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.4</version>
            <configuration>
                <archive>
                    <manifestEntries>
                        <Premain-Class>org.example.Agent</Premain-Class>
                        <Agent-Class>org.example.Agent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

将工程编译后的输出jar包解压,查看META-IN/MANIFEST.MF文件如下,可以看出Java Agent的入口类org.example.Agent已经被写入到文件中。

text
Manifest-Version: 1.0
Premain-Class: org.example.Agent
Archiver-Version: Plexus Archiver
Built-By: root
Agent-Class: org.example.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_261

6.1.2 加载Agent

一个Java Agent既可以在程序运行前加载,也可以在程序运行后动态加载,两者区别主要是Agent初始化的时机不一样。

  • 命令行方式启动

在JVM命令行中添加如下参数:

java
-javaagent:/path/to/your/jarpath[=options]

其中options参数是可选择的,如jacoco agent的启动参数如下:

java
java -javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar application.jar

premain方法允许有如下2种方法签名:

java
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);

当以上两种方式都存在时,带有Instrumentation参数的方法的优先级更高,会被JVM优先调用。这里以命令行形式在SpringBoot应用中加载一个Java Agent。

java
java -javaagent:/path/to/your/my-agent-1.0.jar -jar application.jar

服务启动的终端日志可以看到"premain run"输出。

  • 运行时动态加载

应用程序启动之后,通过JVM提供的Attach机制来加载Java Agent,在前面的章节详细介绍了Attach机制,这里不再重复。

6.1.3 Agent的功能开关

以下是Agent jar包的Manifest Attributes的定义:

  • Premain-Class

指定应用启动前加载的Agent的入口类。

  • Agent-Class

指定运行时加载的Agent的入口类。

  • Boot-Class-Path

指定Agent的依赖jar包的加载路径,该路径下的jar包在Agent加载之前由启动类加载器加载。

  • Can-Redefine-Classes

是否允许Agent对类进行重新定义,默认值为false。

  • Can-Retransform-Classes

是否允许Agent对类进行重新转换,默认值为false。

  • Can-Set-Native-Method-Prefix

是否能设置Agent所需的本机方法前缀。如果设置为true,即允许当前Agent给native方法设置前缀,可以间接实现native方法的字节码的修改。

上面的6个属性在Java Agent中都会使用。可以参考官方文档:

官方文档: https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/compact3-package-summary.html

一个Agent的jar包中可以同时存在Premain-Class和Agent-Class,当Java Agent以命令行方式启动,仅使用Premain-Class,而忽略Agent-Class,以运行时启动Java Agent,则相反。

6.1.4 Java Agent Debug

在分析Java Agent的初始化源码之前,我们先来看下如何对Agent的代码进行debug,这个在定位Agent的问题时非常重要。

  • 启动应用

在Springboot应用中的JVM启动参数中增加jdwp和Java Agent,如下所示:

图6-1 对命令执行底层代码debug

图6-1 对命令执行底层代码debug

启动命令如下:

java
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -javaagent:rce-agent-1.0-SNAPSHOT.jar -jar jetty-demo-0.0.1-SNAPSHOT.jar
  • 在Java Agent工程中增加debug调试参数

图6-2 对命令执行底层代码debug

图6-2 对命令执行底层代码debug

  • 在Java Agent源码上设置断点并运行debug

图6-3 在premain入口处方法增加断点

图6-3 在premain入口处增加断点 premain入口的debug断点比较难设置,可以在进入premain方法时设置一定的延时。

图6-4 在visitMethod方法处增加断点

图6-4 在visitMethod方法处增加断点

6.2 Agent加载源码解析

https://blog.51cto.com/u_16213564/7607442

6.2.1 javaagent参数解析

在JVM启动时,会读取JVM命令行参数如堆空间、元空间和线程栈大小等,初始化时解析的参数众多,因此本文仅关注agent相关的参数如:agentlib、agentpath和javaagent。在JVM启动时将"-javaagent:/path/to/your/agent.jar"添加到启动参数后,就可以被JVM加载和初始化。来看下这部分逻辑的实现。JVM的启动参数都在parse_each_vm_init_arg中解析,下面的代码片段是javaagent的解析代码。

java
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain){
    // ...
    jint parse_result=Arguments::parse(args);
    if(parse_result!=JNI_OK)return parse_result;
    // ...
}

Arguments类负责解析参数,来看下parse成员方法的实现:

jdk11/src/hotspot/share/runtime/arguments.cpp

c++
jint Arguments::parse(const JavaVMInitArgs* initial_cmd_args) {
  assert(verify_special_jvm_flags(), "deprecated and obsolete flag table inconsistent");

  // Initialize ranges, constraints and writeables
  JVMFlagRangeList::init();
  JVMFlagConstraintList::init();
  JVMFlagWriteableList::init();

  // If flag "-XX:Flags=flags-file" is used it will be the first option to be processed.
  const char* hotspotrc = ".hotspotrc";
  bool settings_file_specified = false;
  bool needs_hotspotrc_warning = false;
  ScopedVMInitArgs initial_java_tool_options_args("env_var='JAVA_TOOL_OPTIONS'");
  ScopedVMInitArgs initial_java_options_args("env_var='_JAVA_OPTIONS'");

  // Pointers to current working set of containers
  JavaVMInitArgs* cur_cmd_args;
  JavaVMInitArgs* cur_java_options_args;
  JavaVMInitArgs* cur_java_tool_options_args;

  // Containers for modified/expanded options
  ScopedVMInitArgs mod_cmd_args("cmd_line_args");
  ScopedVMInitArgs mod_java_tool_options_args("env_var='JAVA_TOOL_OPTIONS'");
  ScopedVMInitArgs mod_java_options_args("env_var='_JAVA_OPTIONS'");


  jint code =
      parse_java_tool_options_environment_variable(&initial_java_tool_options_args);
  if (code != JNI_OK) {
    return code;
  }

  code = parse_java_options_environment_variable(&initial_java_options_args);
  if (code != JNI_OK) {
    return code;
  }

  code = expand_vm_options_as_needed(initial_java_tool_options_args.get(),
                                     &mod_java_tool_options_args,
                                     &cur_java_tool_options_args);
  if (code != JNI_OK) {
    return code;
  }

  code = expand_vm_options_as_needed(initial_cmd_args,
                                     &mod_cmd_args,
                                     &cur_cmd_args);
  if (code != JNI_OK) {
    return code;
  }

  code = expand_vm_options_as_needed(initial_java_options_args.get(),
                                     &mod_java_options_args,
                                     &cur_java_options_args);
  if (code != JNI_OK) {
    return code;
  }

  const char* flags_file = Arguments::get_jvm_flags_file();
  settings_file_specified = (flags_file != NULL);

  if (IgnoreUnrecognizedVMOptions) {
    cur_cmd_args->ignoreUnrecognized = true;
    cur_java_tool_options_args->ignoreUnrecognized = true;
    cur_java_options_args->ignoreUnrecognized = true;
  }

  // Parse specified settings file
  if (settings_file_specified) {
    if (!process_settings_file(flags_file, true,
                               cur_cmd_args->ignoreUnrecognized)) {
      return JNI_EINVAL;
    }
  } else {
#ifdef ASSERT
    // Parse default .hotspotrc settings file
    if (!process_settings_file(".hotspotrc", false,
                               cur_cmd_args->ignoreUnrecognized)) {
      return JNI_EINVAL;
    }
#else
    struct stat buf;
    if (os::stat(hotspotrc, &buf) == 0) {
      needs_hotspotrc_warning = true;
    }
#endif
  }

  if (PrintVMOptions) {
    print_options(cur_java_tool_options_args);
    print_options(cur_cmd_args);
    print_options(cur_java_options_args);
  }

  // Parse JavaVMInitArgs structure passed in, as well as JAVA_TOOL_OPTIONS and _JAVA_OPTIONS
  jint result = parse_vm_init_args(cur_java_tool_options_args,
                                   cur_java_options_args,
                                   cur_cmd_args);

  if (result != JNI_OK) {
    return result;
  }

  // Call get_shared_archive_path() here, after possible SharedArchiveFile option got parsed.
  SharedArchivePath = get_shared_archive_path();
  if (SharedArchivePath == NULL) {
    return JNI_ENOMEM;
  }

  // Set up VerifySharedSpaces
  if (FLAG_IS_DEFAULT(VerifySharedSpaces) && SharedArchiveFile != NULL) {
    VerifySharedSpaces = true;
  }

  // Delay warning until here so that we've had a chance to process
  // the -XX:-PrintWarnings flag
  if (needs_hotspotrc_warning) {
    warning("%s file is present but has been ignored.  "
            "Run with -XX:Flags=%s to load the file.",
            hotspotrc, hotspotrc);
  }

  if (needs_module_property_warning) {
    warning("Ignoring system property options whose names match the '-Djdk.module.*'."
            " names that are reserved for internal use.");
  }

#if defined(_ALLBSD_SOURCE) || defined(AIX)  // UseLargePages is not yet supported on BSD and AIX.
  UNSUPPORTED_OPTION(UseLargePages);
#endif

  ArgumentsExt::report_unsupported_options();

#ifndef PRODUCT
  if (TraceBytecodesAt != 0) {
    TraceBytecodes = true;
  }
  if (CountCompiledCalls) {
    if (UseCounterDecay) {
      warning("UseCounterDecay disabled because CountCalls is set");
      UseCounterDecay = false;
    }
  }
#endif // PRODUCT

  if (ScavengeRootsInCode == 0) {
    if (!FLAG_IS_DEFAULT(ScavengeRootsInCode)) {
      warning("Forcing ScavengeRootsInCode non-zero");
    }
    ScavengeRootsInCode = 1;
  }

  if (!handle_deprecated_print_gc_flags()) {
    return JNI_EINVAL;
  }

  // Set object alignment values.
  set_object_alignment();

#if !INCLUDE_CDS
  if (DumpSharedSpaces || RequireSharedSpaces) {
    jio_fprintf(defaultStream::error_stream(),
      "Shared spaces are not supported in this VM\n");
    return JNI_ERR;
  }
  if ((UseSharedSpaces && FLAG_IS_CMDLINE(UseSharedSpaces)) ||
      log_is_enabled(Info, cds)) {
    warning("Shared spaces are not supported in this VM");
    FLAG_SET_DEFAULT(UseSharedSpaces, false);
    LogConfiguration::configure_stdout(LogLevel::Off, true, LOG_TAGS(cds));
  }
  no_shared_spaces("CDS Disabled");
#endif // INCLUDE_CDS

  return JNI_OK;
}
c++
jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args, bool* patch_mod_javabase, JVMFlag::Flags origin) {
  const char* tail;
  // 依次遍历每一个启动参数
  for (int index = 0; index < args->nOptions; index++) {
    const JavaVMOption* option = args->options + index;
    // 其他参数省略
    
    // -agentlib、-agentpath参数解析
    if (match_option(option, "-agentlib:", &tail) ||
          (is_absolute_path = match_option(option, "-agentpath:", &tail))) {
      if(tail != NULL) {
        const char* pos = strchr(tail, '=');
        size_t len = (pos == NULL) ? strlen(tail) : pos - tail;
        char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1, mtArguments), tail, len);
        name[len] = '\0';

        char *options = NULL;
        if(pos != NULL) {
          options = os::strdup_check_oom(pos + 1, mtArguments);
        }
        add_init_agent(name, options, is_absolute_path);
      }
    // -javaagent参数解析
    } else if (match_option(option, "-javaagent:", &tail)) {
      if (tail != NULL) {
        size_t length = strlen(tail) + 1;
        char *options = NEW_C_HEAP_ARRAY(char, length, mtArguments);
        jio_snprintf(options, length, "%s", tail);
        // 添加一个instrument agent
        add_instrument_agent("instrument", options, false);
      }
    } 
    // ...
  return JNI_OK;
}

add_instrument_agent方法主要是将agent参数封装为AgentLibrary对象加入到一个链表中,代码如下:

c++
// -agentlib and -agentpath arguments
static AgentLibraryList _agentList;

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

6.2.2 agentlib 加载

agent 的初始化与

jdk11/src/hotspot/share/runtime/thread.cpp

c++
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
    
    // vm的其他部分初始化代码
    
    // 启动agent
    if (Arguments::init_agents_at_startup()) {
        create_vm_init_agents();
    }
}

create_vm_init_agents方法的实现如下:

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

  JvmtiExport::enter_onload_phase();
  
  // 遍历agentList链表并调用Agent_OnLoad完成agent初始化
  for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
    OnLoadEntry_t  on_load_entry = lookup_agent_on_load(agent);

    if (on_load_entry != NULL) {
      // 调用 Agent_OnLoad 函数
      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();
}

6.2.2 方法调用

6.2.3 JPLISAgent

6.3 JVMTI 接口

6.3.1 JVMTI 1.0版本

参考文档:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/index.html

6.3.2 JVMTI 1.1版本

https://docs.oracle.com/en/java/javase/20/docs/specs/jvmti.html

6.5 实现一个native agent