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动态链接库的实现。