大家好,欢迎来到IT知识分享网。
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 . * * 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动态链接库的实现。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/176995.html