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类的代码如下:
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-Class
和Agent-Class
的配置值为上面Agent类的全限定名称。配置如下:
<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
已经被写入到文件中。
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命令行中添加如下参数:
-javaagent:/path/to/your/jarpath[=options]
其中options参数是可选择的,如jacoco agent的启动参数如下:
java -javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar application.jar
premain方法允许有如下2种方法签名:
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
当以上两种方式都存在时,带有Instrumentation参数的方法的优先级更高,会被JVM优先调用。这里以命令行形式在SpringBoot应用中加载一个Java Agent。
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
启动命令如下:
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
- 在Java Agent源码上设置断点并运行debug
图6-3 在premain入口处方法增加断点
premain入口的debug断点比较难设置,可以在进入premain方法时设置一定的延时。
图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的解析代码。
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
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;
}
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对象加入到一个链表中,代码如下:
// -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
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
// vm的其他部分初始化代码
// 启动agent
if (Arguments::init_agents_at_startup()) {
create_vm_init_agents();
}
}
create_vm_init_agents方法的实现如下:
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