Java Agent Principle Analysis
Diagnostic tools commonly used for performance issues, such as arthas and btrace, are all implemented based on Java Agent. A Java Agent is a JAR package, but its startup method differs from regular JAR packages. For regular JAR packages, startup is achieved by specifying the main function of a class, whereas a Java Agent cannot run independently—it must attach to a running Java application. This chapter first implements a simple Java Agent, then analyzes the initialization and underlying implementation source code of Java Agent.
6.1 Java Agent Basics
6.1.1 Implementing a Simple Java Agent
The code for the Agent class is as follows:
package org.example;import java.lang.instrument.Instrumentation;
public class Agent { // Loaded via VM parameters, executed before the main method of the Java program public static void premain(String agentArgs, Instrumentation inst) { System.out.println("premain run"); } // Loaded via Attach, executed after the Java program starts public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("agentmain run"); }}
You need to implement specific Agent logic in either the agentmain or premain methods, such as reading thread states, monitoring data, and modifying class bytecode.Due to the special nature of Java Agent, some additional configurations are required. You need to create a MANIFEST.MF file in the META-INF directory, which can be generated either manually or automatically using a Maven plugin. Here we recommend using the Maven plugin for automatic generation. Add the following plugin configuration in the pom.xml file, where the values for Premain-Class
and Agent-Class
should be the fully qualified name of the Agent class mentioned above. The configuration is as follows:
<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>
After compiling the project, extract the output jar file and check the META-INF/MANIFEST.MF file as shown below. You can see that the Java Agent entry class org.example.Agent
has been written into the file.
Manifest-Version: 1.0Premain-Class: org.example.AgentArchiver-Version: Plexus ArchiverBuilt-By: rootAgent-Class: org.example.AgentCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: trueCreated-By: Apache Maven 3.6.3Build-Jdk: 1.8.0_261
6.1.2 Loading Agent
A Java Agent can be loaded either before program execution or dynamically during runtime. The main difference lies in the initialization timing of the Agent.
- Command-line startupAdd the following parameter to the JVM command line:
-javaagent:/path/to/your/jarpath[=options]
The options parameter is optional. For example, the startup parameters for the Jacoco agent are as follows:
java -javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar application.jar
The premain method allows the following two method signatures:
public static void premain(String agentArgs, Instrumentation inst);public static void premain(String agentArgs);
When both methods exist, the one with the Instrumentation parameter takes precedence and will be called first by the JVM. Here is an example of loading a Java Agent in a SpringBoot application via the command line:
java -javaagent:/path/to/your/my-agent-1.0.jar -jar application.jar
The terminal logs during service startup will display the “premain run” output.
- Dynamic Loading at Runtime
After the application starts, load the Java Agent using the Attach mechanism provided by the JVM. The Attach mechanism has been detailed in previous chapters and will not be repeated here.
6.1.3 Agent Feature Switches
The following are definitions of Manifest Attributes for an Agent jar package:
- Premain-Class
Specifies the entry class of the Agent to be loaded before application startup.
- Agent-Class
Specifies the entry class of the Agent to be loaded at runtime.
- Boot-Class-Path
Specifies the loading path for the Agent’s dependent jar packages. The jar packages in this path are loaded by the bootstrap classloader before the Agent is loaded.
- Can-Redefine-Classes
Indicates whether the Agent is allowed to redefine classes. The default value is false.
- Can-Retransform-Classes
Indicates whether the Agent is allowed to retransform classes. The default value is false.
- Can-Set-Native-Method-Prefix
Indicates whether the Agent can set a prefix for native methods. If set to true, it allows the current Agent to set prefixes for native methods, indirectly enabling bytecode modification of native methods.
All six attributes above are used in Java Agents. Refer to the official documentation:
Official Documentation: https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/compact3-package-summary.html
An Agent jar package can contain both Premain-Class and Agent-Class. When a Java Agent is started via the command line, only Premain-Class is used, and Agent-Class is ignored. Conversely, when starting a Java Agent at runtime, only Agent-Class is used.
6.1.4 Java Agent Debugging
Before analyzing the initialization source code of a Java Agent, let’s first look at how to debug an Agent’s code, which is crucial for troubleshooting Agent-related issues.
- Starting the Application
Add jdwp and Java Agent parameters to the JVM startup parameters in a SpringBoot application as shown below:
Figure 6-1 Debugging underlying command execution code
The startup command is as follows:
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
- Add debug parameters to the Java Agent project
Figure 6-2 Debugging the underlying command execution code
- Set breakpoints and run debug on Java Agent source code
Figure 6-3 Adding breakpoints at the premain entry method
Setting debug breakpoints at the premain entry can be challenging. You may add a slight delay when entering the premain method.
Figure 6-4 Adding breakpoints at the visitMethod method
6.2 Agent Loading Source Code Analysis
https://blog.51cto.com/u_16213564/7607442
6.2.1 javaagent Parameter Parsing
During JVM startup, it reads JVM command-line parameters such as heap space, metaspace, and thread stack size. Numerous parameters are parsed during initialization, so this article focuses only on agent-related parameters like: agentlib, agentpath, and javaagent. When “-javaagent:/path/to/your/agent.jar” is added to the startup parameters during JVM launch, it can be loaded and initialized by the JVM. Let’s examine the implementation of this logic. All JVM startup parameters are parsed in parse_each_vm_init_arg. The following code snippet shows the parsing code for javaagent.
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain){ // ... jint parse_result=Arguments::parse(args); if(parse_result!=JNI_OK)return parse_result; // ...}
The Arguments class is responsible for parameter parsing. Let’s look at the implementation of the parse member method:
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; }```c 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); }``````c // 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();```The provided content is already in English and consists of C++ code blocks with conditional compilation directives and configuration settings. Since this is code and not Chinese text requiring translation, I'll return it exactly as provided:
```c++#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; // Iterate through each startup parameter for (int index = 0; index < args->nOptions; index++) { const JavaVMOption* option = args->options + index; // Other parameters omitted
// -agentlib, -agentpath parameter parsing 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 parameter parsing } 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); // Add an instrument agent add_instrument_agent("instrument", options, false); } } // ... return JNI_OK;}
The add_instrument_agent
method primarily encapsulates agent parameters into AgentLibrary objects and adds them to a linked list. The code is as follows:
// -agentlib and -agentpath argumentsstatic AgentLibraryList _agentList;``````c++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 Loading
Agent initialization and
jdk11/src/hotspot/share/runtime/thread.cpp
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
// Other VM initialization code
// Launch agents if (Arguments::init_agents_at_startup()) { create_vm_init_agents(); }}
The implementation of create_vm_init_agents method is as follows:
void Threads::create_vm_init_agents() { extern struct JavaVM_ main_vm; AgentLibrary* agent;
JvmtiExport::enter_onload_phase();
// Traverse the agentList linked list and call Agent_OnLoad to complete agent initialization for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) { OnLoadEntry_t on_load_entry = lookup_agent_on_load(agent);
if (on_load_entry != NULL) { // Call 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();}