javaagent介绍 jdk提供了一种强大的可以对已有class代码进行运行时注入修改的能力。javaagent可以在启动时通过javaagent:agentJarPath或运行时attach加载agent包的方式使用,通过javaagent我们可以对特定的类进行字节码修改,在方法执行前后注入特定的逻辑。通过字节码修改,可以实现监控tracing、性能分析、在线诊断、代码热更新热部署等等各种能力。监控tracing:分布式tracing框架的Java类库(比如skywalking,brave,opentracingjava)常使用javaagent实现,因为tracing需要在各个第三方框架内注入tracing数据的统计收集逻辑,比如要在grpc、kafka中发送消息前后收集tracing日志,但是这些第三方的jar包我们不方便修改它们的代码,使用javaagent就成为了很好的选择。性能分析:很多性能分析软件例如jprofiler使用javaagent技术,一般分析分为sampling和instrumentation两种方式,sample是通过类似jstack的方式采集方法的执行栈,instrumentatino就是修改字节码来收集方法的执行次数、耗时等信息。在线诊断:arthas这样的软件使用javaagent技术在运行时将诊断逻辑注入到已有代码中,实现watch,trace等功能代码热更新、热部署:通过javaagent技术,还能够实现Java代码的热更新,减少Java服务重启次数,提升开发效率,比如开源的https:github。comHotswapProjectsHotswapAgent和https:github。comdcevmdcevm使用编写、打包、使用javaagent 我们以〔javaagentexample〕(https:github。comliuzhengyangjavaagentexample)项目为例使用字节码实现一个最简单的AOP功能,在某个方法执行前打印字符串。 编写javaagent需要在jar包中创建METAINFMANIFEST。MF来配置agent的入口类等信息,通过maven的mavenassemblyplugin插件把resources文件夹下METAINFMANIFEST。MF文件打包到jar包中。( mavenpom相关配置示例如下。(除了mavenassemblyplugin,还可以用mavenshadeplugin)buildpluginsplugingroupIdorg。apache。maven。pluginsgroupIdmavencompilerpluginartifactIdconfigurationsource1。8sourcetarget1。8targetconfigurationpluginplugingroupIdorg。apache。maven。pluginsgroupIdmavensourcepluginartifactIdversion3。0。1versionexecutionsexecutionidattachsourcesidphaseverifyphasegoalsgoaljarnoforkgoalgoalsexecutionexecutionspluginplugingroupIdorg。apache。maven。pluginsgroupIdmavenassemblypluginartifactIdversion2。6versionconfigurationdescriptorRefsdescriptorRefjarwithdependenciesdescriptorRefdescriptorRefsmanifestFilesrcmainresourcesMETAINFMANIFEST。MFmanifestFilearchiveconfigurationexecutionsexecutionidassembleallidphasepackagephasegoalsgoalsinglegoalgoalsexecutionexecutionspluginpluginsresourcesresourcedirectory{basedir}srcmainresourcesdirectoryresourceresourcedirectory{basedir}srcmainjavadirectoryresourceresourcesbuild 同时我们还需要在pom。xml添加我们要使用的字节码修改框架asmdependencygroupIdorg。ow2。asmgroupIdasmallartifactIdversion5。1versiondependency 然后我们添加MANIFEST。MF文件(在resourcesMETAINF文件夹下,如果没有则进行创建) PremainClass和AgentClass都配置成agent的入口类。CanRedefineClasses表示agent是否需要redefine的能力,默认为false,还有一个CanRetransformClasses配置,我们这里虽然声明了true但是其实没有使用redfine能力。ManifestVersion:1。0PremainClass:com。lzy。javaagent。AgentMainAgentClass:com。lzy。javaagent。AgentMainCanRedefineClasses:true 最后编写Agent入口类,也就是上面的com。lzy。javaagent。AgentMain javaagent的核心功能集中在通过premainagentmain获得的Instrumentation对象上,通过Instrumentation对象可以添加ClassFileTransformer、调用redefineretransform方法,以实现修改类代码的能力。我们要实现的简单的AOP,就是在类加载前,给Instrumentation添加我们的自定义的ClassFileTransformer,ClassFileTransformer读取加载的类,然后通过字节码工具进行解析、修改,在AOP目标类的方法的执行前后打印我们想打印的字符串。具体实现如下,其中ClassFileTransformer使用javassist框架进行字节码修改,后续的文章我们会详细介绍javassist的使用。 AgentMain接收Instrumentation和String参数,这里我们把String参数用来指定AOP目标类publicclassAgentMain{publicstaticvoidpremain(StringagentOps,Instrumentationinst){instrument(agentOps,inst);}publicstaticvoidagentmain(StringagentOps,Instrumentationinst){instrument(agentOps,inst);}agentOpsisaoptargetclassnameprivatestaticvoidinstrument(StringagentOps,Instrumentationinst){System。out。println(agentOps);inst。addTransformer(newAOPTransformer(agentOps));}} AOPTransformer实现ClassFileTransformer,在加载指定的类时,对类进行修改在方法调用前增加代码,打印方法名。authorliuzhengyang2022413publicclassAOPTransformerimplementsClassFileTransformer{privatefinalStringclassNpublicAOPTransformer(StringclassName){this。classNameclassN}注意这里的className是abC这样的而不是a。b。COverridepublicbyte〔〕transform(ClassLoaderloader,StringclassName,C?classBeingRedefined,ProtectionDomainprotectionDomain,byte〔〕classfileBuffer)throwsIllegalClassFormatException{if(classNamenull){返回null表示不修改类字节码,和返回classfileBuffer是一样的效果。}if(className。equals(this。className。replace(。,))){ClassPoolclassPoolClassPool。getDefault();classPool。appendClassPath(newLoaderClassPath(loader));classPool。appendSystemPath();try{CtClassctClassclassPool。makeClass(newByteArrayInputStream(classfileBuffer));CtMethod〔〕declaredMethodsctClass。getDeclaredMethods();for(CtMethoddeclaredMethod:declaredMethods){declaredMethod。insertBefore(System。out。println(beforeinvokedeclaredMethod。getName()););}returnctClass。toBytecode();}catch(Exceptione){e。printStackTrace();}}returnclassfileB}} 然后通过mvncleanpackage进行打包,在target目录下可以得到一个fatjar(包含javassist等依赖),名为javaagent1。0SNAPSHOTjarwithdependencies。jar 然后我们就可以通过javaagent:tmpjavaagent1。0SNAPSHOTjarwithdependencies。jarcom。lzy。javaagent。Test来使用agent了,注意javaagent:后面要换成自己的agentjar包的绝对路径,后面是传入的参数,我们这里的com。lzy。javaagent。Test是我们要aop的类。如果是IDEA中使用,可以 例如我们编写一个简单的Test类packagecom。lzy。authorliuzhengyang2022413publicclassTest{publicvoidhello(){System。out。println(hello);}publicstaticvoidmain(String〔〕args){newTest()。hello();}} 在idea中添加先运行一次,然后修改RunConfiguration,在vmoptions中添加javaagent:UsersliuzhengyangCodeopensourcejavaagentexampletargetjavaagent1。0SNAPSHOTjarwithdependencies。jarcom。lzy。javaagent。Test运行,就可以看到AOP的效果了com。lzy。javaagent。Testbeforeinvokemainbeforeinvokehellohello通过bytebuddy获取Instrumentation 有时修改javaagent参数不是特别方便,比如使用方可能不方便或不知道怎么修改启动参数,有没有通过maven依赖代码调用的方式使用javaagent呢?通过bytebuddy可以实现这一功能。 首先pom依赖中添加bytebuddyagent的maven依赖dependencygroupIdnet。bytebuddygroupIdbytebuddyagentartifactIdversion1。11。22versiondependency 然后通过ByteBuddyAgent。install(),就可以很方便的获得Instrumentation对象,接下来就可以添加ClassFileTransformer、调用redefine等等。 关于bytebuddy的使用和实现原理,我们会在后面文章中详细介绍。publicclassTestByteBuddyInstall{publicstaticvoidmain(String〔〕args){InstrumentationinstallByteBuddyAgent。install();System。out。println(install);install。addTransformer();}}Instrumentation接口介绍 我们对java。lang。instrument。Instrumentation类的重要方法进行一下介绍 方法 说明 voidaddTransformer(ClassFileTransformertransformer) 添加一个Transformer voidaddTransformer(ClassFileTransformertransformer,booleancanRetransform) 添加一个Transformer,如果canRetransform为true这个transformer在类被retransform的时候会调用 voidappendToBootstrapClassLoaderSearch(JarFilejarfile) 添加一个jar包让bootstrapclassloader能够搜索到 voidappendToSystemClassLoaderSearch(JarFilejarfile) 添加一个jar包让systemclassloader能够搜索到 Class〔〕getAllLoadedClasses() 获取当前所有已经加载的类 Class〔〕getInitiatedClasses(ClassLoaderloader) 获取某个classloader已经初始化过的类 longgetObjectSize(ObjectobjectToSize) 获取某个对象的大小(不包含引用的传递大小,比如一个String字段,只计算这个字段的引用4byte) voidredefineClasses(ClassDefinition。。。definitions) 对某个类进行redefine修改代码,注意默认jdk只能修改方法体,不能进行增减字段方法等,dcevmjdk可以实现更强大的修改功能 booleanremoveTransformer(ClassFileTransformertransformer) 从Instrumentation中删除Transformer voidretransformClasses(C?。。。classes) 让一个已经加载的类重新transform,不过在retransform过程中和redefine一样,不能对类结构进行变更,只能修改方法体javaagent使用注意事项javaagent的premain和agentmain的类是通过SystemClassLoader(AppClassLoader)加载的,所以如果要和业务代码通信,需要考虑classloader不同的情况,一般要通过反射(可以传入指定classloader加载类)和业务代码通信。注意依赖冲突的问题,比如agent的fatjar中包含了某个第三方的类,业务代码中也包含了相同的第三方但是不同版本的类,由于classloader存在父类优先委派加载的情况,可能会导致类加载异常,所以一般会通过shaded修改第三方类库的包名或者通过classloader隔离实现METAINFMANIFEST。MF文件 javaagent在打包时,按照规范需要在jar包中的METAINFMANIFEST。MF文件中声明javaagent的配置信息,其中最关键的是AgentClass、PremainClass,这两个表示使用动态attach和javaagent启动时调用的类,JVM会在这个类中寻找对应的agentmain和premain方法执行。CanRedefineClasses、CanRetransformClasses表示此javaagent是否需要使用Instrumentation的redefine和retransform的能力。修改类的字节码有两个时机,一个javaagent通过Instrumentation。addTransformer方法注入ClassFileTransformer,在类加载时,jvm会调用各个ClassFileTransformer,ClassFileTransformer可以修改类的字节码,但是如果要在类已经加载后再去修改它的字节码,就需要使用redefine和retransform。ManifestVersion:1。0ArchiverVersion:PlexusArchiverCreatedBy:ApacheMaven3。6。3BuiltBy:liuzhengyangBuildJdk:11。0。11AgentClass:org。hotswap。agent。HotswapAgentCanRedefineClasses:trueCanRetransformClasses:trueImplementationTitle:javareloadagentassemblyImplementationVersion:1。0SNAPSHOTPremainClass:org。hotswap。agent。HotswapAgentSpecificationTitle:javareloadagentassemblySpecificationVersion:1。0SNAPSHOTjavaagent:执行流程参数解析 例如当我们通过javaagent:UsersliuzhengyangCodeopensourcejavareloadagentjavareloadagentassemblytargetjavareloadagent。jar启动时, 以下代码位于jdk的arguments。cpp中,jvm解析传入的启动参数,对于javaagent参数,会解析agentjar包路径和其他参数,并放到AgentLibraryList中。AgentLibraryList是AgentLibrary的链表,AgentLibrary包含agent的名称参数等信息。elseif(matchoption(option,javaagent:,tail)){if!INCLUDEJVMTIjiofprintf(defaultStream::errorstream(),InstrumentationagentsarenotsupportedinthisVM);returnJNIERR;elseif(tail!NULL){sizetlengthstrlen(tail)1;charoptionsNEWCHEAPARRAY(char,length,mtArguments);jiosnprintf(options,length,s,tail);addinstrumentagent(instrument,options,false);javaagentsneedmodulejava。instrumentif(!createnumberedproperty(jdk。module。addmods,java。instrument,addmodscount)){returnJNIENOMEM;}}endifvoidArguments::addinstrumentagent(constcharname,charoptions,boolabsolutepath){agentList。add(newAgentLibrary(name,options,absolutepath,NULL,true));}agentlibandagentpathargumentsstaticAgentLibraryListagentLagentLibrary加载使用 解析完启动参数后,jvm会创建vm,agentLibrary也是在这个过程中加载的。 createvm方法判断Arguments::initagentsatstartup()为true(AgentLibraryList不为空列表),则执行createvminitagents。 以下代码位于thread。cpp中。jintThreads::createvm(JavaVMInitArgsargs,boolcanTryAgain){externvoidJDKVersioninit();Preinitializeversioninfo。VMVersion::earlyinitialize();省略其他代码。。。LaunchagentlibagentpathandconvertedXrunagentsif(Arguments::initagentsatstartup()){createvminitagents();}省略其他代码。。。} createvminitagents方法负责初始化各个AgentLibrary,lookupagentonload负责查找加载AgentLibrary对应的JVMTI动态链接库,然后调用对应JVMTI动态链接库的onloadentry回调方法voidThreads::createvminitagents(){externstructJavaVMAgentLJvmtiExport::enteronloadphase();for(agentArguments::agents();agent!NULL;agentagentnext()){CDSdumpingdoesnotsupportnativeJVMTIagent。CDSdumpingsupportsJavaagentiftheAllowArchivingWithJavaAgentdiagnosticoptionisspecified。if(Arguments::isdumpingarchive()){if(!agentisinstrumentlib()){vmexitduringcdsdumping(CDSdumpingdoesnotsupportnativeJVMTIagent,name,agentname());}elseif(!AllowArchivingWithJavaAgent){vmexitduringcdsdumping(MustenableAllowArchivingWithJavaAgentinordertorunJavaagentduringCDSdumping);}}OnLoadEntrytonloadentrylookupagentonload(agent);if(onloadentry!NULL){InvoketheAgentOnLoadfunctionjinterr(onloadentry)(mainvm,agentoptions(),NULL);if(err!JNIOK){vmexitduringinitialization(agentlibraryfailedtoinit,agentname());}}else{vmexitduringinitialization(CouldnotfindAgentOnLoadfunctionintheagentlibrary,agentname());}}JvmtiExport::enterprimordialphase();} lookupagentonload方法负责查找对应的jvmti动态链接库,对于javaagent,jvm中已经内置了对应的动态库名为instrument,位于jdk的lib文件夹下,比如mac下是liblibinstrument。dylib,linux中是liblibinstrument。so。Findacommandlineagentlibraryandreturnitsentrypointforagentlib:agentpath:Xrunnumsymbolentriesmustbepassedinsinceonlythecallerknowsthenumberofsymbolsinthearray。staticOnLoadEntrytlookuponload(AgentLibraryagent,constcharonloadsymbols〔〕,sizetnumsymbolentries){OnLoadEntrytonloadentryNULL;voidlibraryNULL;if(!agentvalid()){charbuffer〔JVMMAXPATHLEN〕;charebuf〔1024〕;constcharnameagentname();constcharmsgCFirstchecktoseeifagentisstaticallylinkedintoexecutableif(os::findbuiltinagent(agent,onloadsymbols,numsymbolentries)){libraryagentoslib();}elseif(agentisabsolutepath()){libraryos::dllload(name,ebuf,sizeofebuf);if(libraryNULL){constcharsubmsginabsolutepath,witherror:;sizetlenstrlen(msg)strlen(name)strlen(submsg)strlen(ebuf)1;charbufNEWCHEAPARRAY(char,len,mtThread);jiosnprintf(buf,len,ssss,msg,name,submsg,ebuf);Ifwecantfindtheagent,exit。vmexitduringinitialization(buf,NULL);FREECHEAPARRAY(char,buf);}}else{Trytoloadtheagentfromthestandarddlldirectoryif(os::dlllocatelib(buffer,sizeof(buffer),Arguments::getdlldir(),name)){libraryos::dllload(buffer,ebuf,sizeofebuf);}if(libraryNULL){Trythelibrarypathdirectory。if(os::dllbuildname(buffer,sizeof(buffer),name)){libraryos::dllload(buffer,ebuf,sizeofebuf);}if(libraryNULL){constcharsubmsgonthelibrarypath,witherror:;constcharsubmsg2Modulejava。instrumentmaybemissingfromruntimeimage。;sizetlenstrlen(msg)strlen(name)strlen(submsg)strlen(ebuf)strlen(submsg2)1;charbufNEWCHEAPARRAY(char,len,mtThread);if(!agentisinstrumentlib()){jiosnprintf(buf,len,ssss,msg,name,submsg,ebuf);}else{jiosnprintf(buf,len,sssss,msg,name,submsg,ebuf,submsg2);}Ifwecantfindtheagent,exit。vmexitduringinitialization(buf,NULL);FREECHEAPARRAY(char,buf);}}}agentsetoslib(library);agentsetvalid();}FindtheOnLoadfunction。onloadentryCASTTOFNPTR(OnLoadEntryt,os::findagentfunction(agent,false,onloadsymbols,numsymbolentries));} instrument动态链接库的实现位于javainstrumentatsharenativelibinstrument入口为InvocationAdapter。c,onloadentry方法实现是DEFAgentOnLoad方法。createNewJPLISAgent是创建一个JPLISAgent(JavaProgrammingLanguageInstrumentationServices)创建完成JPLISAgent后,会读取保存premainClass、jarfile、bootClassPath等信息。JNIEXPORTjintJNICALLDEFAgentOnLoad(JavaVMvm,chartail,voidreserved){JPLISInitializationErroriniterrorJPLISINITERRORNONE;jintresultJNIOK;JPLISAgentagentNULL;initerrorcreateNewJPLISAgent(vm,agent);if(initerrorJPLISINITERRORNONE){intoldLen,newLjarAcharpremainCcharbootClassPParsejarfile〔options〕intojarfileandoptionsif(parseArgumentTail(tail,jarfile,options)!0){fprintf(stderr,javaagent:memoryallocationfailure。);returnJNIERR;}AgentOnLoadisspecifiedtoprovidetheagentoptionsargumenttailinmodifiedUTF8。Howeverfor1。5。0thisisactuallyintheplatformencodingsee5049313。Openzipjarfileandparsearchive。Ifcantbeopenedornotazipfilereturnerror。AlsoifPremainClassattributeisntpresentwereturnanerror。attributesreadAttributes(jarfile);if(attributesNULL){fprintf(stderr,ErroropeningzipfileorJARmanifestmissing:s,jarfile);free(jarfile);if(options!NULL)free(options);returnJNIERR;}premainClassgetAttribute(attributes,PremainClass);if(premainClassNULL){fprintf(stderr,FailedtofindPremainClassmanifestattributeins,jarfile);free(jarfile);if(options!NULL)free(options);freeAttributes(attributes);returnJNIERR;}SavethejarfilenameagentmJThevalueofthePremainClassattributebecomestheagentclassname。ThemanifestisinUTF8soneedtoconverttomodifiedUTF8(seeJNIspec)。oldLen(int)strlen(premainClass);newLenmodifiedUtf8LengthOfUtf8(premainClass,oldLen);if(newLenoldLen){premainClassstrdup(premainClass);}else{charstr(char)malloc(newLen1);if(str!NULL){convertUtf8ToModifiedUtf8(premainClass,oldLen,str,newLen);}premainC}if(premainClassNULL){fprintf(stderr,javaagent:memoryallocationfailed);free(jarfile);if(options!NULL)free(options);freeAttributes(attributes);returnJNIERR;}IftheBootClassPathattributeisspecifiedthenweprocesseachrelativeURLandaddittothebootclasspath。bootClassPathgetAttribute(attributes,BootClassPath);if(bootClassPath!NULL){appendBootClassPath(agent,jarfile,bootClassPath);}ConvertJARattributesintoagentcapabilitiesconvertCapabilityAttributes(attributes,agent);Track(record)theagentclassnameandoptionsdatainiterrorrecordCommandLineData(agent,premainClass,options);Cleanupif(options!NULL)free(options);freeAttributes(attributes);free(premainClass);}switch(initerror){caseJPLISINITERRORNONE:resultJNIOK;caseJPLISINITERRORCANNOTCREATENATIVEAGENT:resultJNIERR;fprintf(stderr,java。lang。instrumentjavaagent:cannotcreatenativeagent。);caseJPLISINITERRORFAILURE:resultJNIERR;fprintf(stderr,java。lang。instrumentjavaagent:initializationofnativeagentfailed。);caseJPLISINITERRORALLOCATIONFAILURE:resultJNIERR;fprintf(stderr,java。lang。instrumentjavaagent:allocationfailure。);caseJPLISINITERRORAGENTCLASSNOTSPECIFIED:resultJNIERR;fprintf(stderr,javaagent:agentclassnotspecified。);default:resultJNIERR;fprintf(stderr,java。lang。instrumentjavaagent:unknownerror);}}调用premain方法 在Thread::createvm方法中,会调用postvminitialized,回调各个JVMTI动态链接库,其中instrument中NotifyJVMTIagentsthatVMinitializationiscompletenopifnoagents。JvmtiExport::postvminitialized(); 其中instrument的JVMTI入口在InvocationAdapter。c的eventHandlerVMInit方法,eventHandlerVMInit中会调用JPLISAgent的processJavaStart方法来启动javaagent中的premain方法。JVMTIcallbacksupportWehavetwostagesofcallbacksupport。AtOnLoadtime,weinstallaVMInithandler。WhentheVMInithandlerruns,weremovetheVMInithandlerandinstallaClassFileLoadHookhandler。voidJNICALLeventHandlerVMInit(jvmtiEnvjvmtienv,JNIEnvjnienv,jthreadthread){JPLISEnvironmentenvironmentNULL;jbooleansuccessJNIFALSE;environmentgetJPLISEnvironment(jvmtienv);processthepremaincallsonthealltheJPLagentsif(environmentNULL){abortJVM(jnienv,JPLISERRORMESSAGECANNOTSTART,gettingJPLISenvironmentfailed);}jthrowableoutstandingExceptionNULL;AddthejarfiletothesystemclasspathJPLISAgentagentenvironmentmAif(appendClassPath(agent,agentmJarfile)){fprintf(stderr,UnabletoaddstosystemclasspaththesystemclassloaderdoesnotdefinetheappendToClassPathForInstrumentationmethodorthemethodfailed,agentmJarfile);free((void)agentmJarfile);abortJVM(jnienv,JPLISERRORMESSAGECANNOTSTART,appendingtosystemclasspathfailed);}free((void)agentmJarfile);agentmJarfileNULL;outstandingExceptionpreserveThrowable(jnienv);successprocessJavaStart(environmentmAgent,jnienv);restoreThrowable(jnienv,outstandingException);ifwefailtostartcleanly,bringdowntheJVMif(!success){abortJVM(jnienv,JPLISERRORMESSAGECANNOTSTART,processJavaStartfailed);}} processJavaStart负责调用agentjar包中的premain方法。createInstrumentationImpl创建Instrumentation类的实例(sun。instrument。InstrumentationImpl)startJavaAgent会调用agent中的premain方法,传入Instrumentation类实例和agent参数。Ifthiscallfails,theJVMlaunchwillultimatelybeaborted,sowedonthavetobesupercarefultocleanupinpartialfailurecases。jbooleanprocessJavaStart(JPLISAgentagent,JNIEnvjnienv){OK,Javaisupnow。WecanstarteverythingthatneedsJava。FirstmakeourfallbackInternalErrorthrowable。resultinitializeFallbackError(jnienv);jplisassertmsg(result,fallbackinitfailed);NowmaketheInstrumentationImplinstance。if(result){resultcreateInstrumentationImpl(jnienv,agent);jplisassertmsg(result,instrumentationinstancecreationfailed);}RegisterahandlerforClassFileLoadHook(withoutenablingthisevent)。TurnofftheVMInithandler。if(result){resultsetLivePhaseEventHandlers(agent);jplisassertmsg(result,settingoflivephaseVMhandlersfailed);}LoadtheJavaagent,andcallthepremain。if(result){resultstartJavaAgent(agent,jnienv,agentmAgentClassName,agentmOptionsString,agentmPremainCaller);jplisassertmsg(result,agentloadpremaincallfailed);}Finallysurrenderallofthetrackingdatathatwedontneedanymore。Ifsomethingiswrong,skipit,wewillbeabortingtheJVManyway。if(result){deallocateCommandLineData(agent);}}CanRedefineClasses和CanRetransformClasses的作用 jvmti运行时attach加载agent 在启动时通过javaagent加载agent在一些情况下不太方便,比如有时候我们想对运行中的程序进行一些类的变更,比如进行性能分析或者程序诊断,如果要修改启动参数重启,可能会导致现场被破坏,修改参数重启也不是很方便,这时jdk提供的动态attach加载agent功能就非常方便了。arthas和jprofiler均能这种方式。 attach和loadAgent代码实例如下,首先通过VirtualMachine。attachattach到本机的某个java进程,得到VirtualMachine,然后调用VirtualMachine的loadAgent方法加载调用具体的路径的javaagentjar包。 这个是由jdk的AttachListener实现的,除了attach后加载javaagent,jdk中的jstack,jcmd等命令也都是使用AttachListener机制和jvm通信的。Stringpid要attach的目标进程StringagentPathjavaagentjar包的绝对路径;StringagentOptions可选的传给agentmain方法的参数;try{VirtualMachinevirtualMachineVirtualMachine。attach(pid);virtualMachine。loadAgent(agentPath,agentOptions);virtualMachine。detach();}catch(Exceptione){e。printStackTrace();}attach客户端 jvm在tmpdir目录下(linux下是tmp)创建。javapid文件(是进程id)用来和客户端通信,默认情况下不会提前创建,客户端会通过向目标java进程发送QUIT信号,java进程收到QUIT后会创建这个通信文件。VirtualMachineImpl(AttachProviderprovider,Stringvmid)throwsAttachNotSupportedException,IOException{super(provider,vmid);try{pidInteger。parseInt(vmid);}catch(NumberFormatExceptionx){thrownewAttachNotSupportedException(Invalidprocessidentifier);}Findthesocketfile。IfnotfoundthenweattempttostarttheattachmechanisminthetargetVMbysendingitaQUITsignal。Thenweattempttofindthesocketfileagain。FilesocketfilenewFile(tmpdir,。javapidpid);socketpathsocketfile。getPath();if(!socketfile。exists()){FilefcreateAttachFile(pid);sendQuitTo(pid);。。。intssocket();try{connect(s,socketpath);}finally{close(s);}} 创建完VirtualMachine以及socket通信后,就可以向jvm发送消息了。loadAgent调用loadAgentLibrary传入instrument表示使用这个JVMTI动态链接库,并且传入args参数。publicvoidloadAgent(Stringagent,Stringoptions)throwsAgentLoadException,AgentInitializationException,IOException{。。。Sif(options!null){}try{loadAgentLibrary(instrument,args);}catch(AgentInitializationExceptionx){。。。} loadAgentLibraryprivatevoidloadAgentLibrary(StringagentLibrary,booleanisAbsolute,Stringoptions)throwsAgentLoadException,AgentInitializationException,IOException{InputStreaminexecute(load,agentLibrary,isAbsolute?true:false,options);。。。} execute负责通过。javapid这个socket文件和jvm进行通信发送cmd和相关参数。InputStreamexecute(Stringcmd,Object。。。args)throwsAgentLoadException,IOException{intssocket();connecttotargetVMtry{connect(s,socketpath);}catch(IOExceptionx){close(s);}try{writeString(s,PROTOCOLVERSION);writeString(s,cmd);for(inti0;i3;i){if(iargs。lengthargs〔i〕!null){writeString(s,(String)args〔i〕);}else{writeString(s,);}}。。。}AttachListener AttachListener提供jvm外部和jvm通信的通道。 AttachListener初始化时默认不启动(降低资源消耗),Attach客户端会先判断是否有。javapid文件,如果没有向java进程发送QUIT信号,jvm监听这个信号,如果没有启动AttachListener则会进行AttachListener创建初始化os。cpp中的signalthreadentry方法switch(sig){caseSIGBREAK:{if(!DisableAttachMechanism){AttachListenerStatecurstateAttachListener::transitstate(ALINITIALIZING,ALNOTINITIALIZED);if(curstateALINITIALIZING){}elseif(curstateALNOTINITIALIZED){if(AttachListener::isinittrigger()){}voidAttachListener::init(){constcharthreadname〔〕AttachLHandlestringjavalangString::createfromstr(threadname,THREAD);if(hasiniterror(THREAD)){setstate(ALNOTINITIALIZED);}Handlethreadgroup(THREAD,Universe::systemthreadgroup());HandlethreadoopJavaCalls::constructnewinstance(SystemDictionary::Threadklass(),vmSymbols::threadgroupstringvoidsignature(),threadgroup,string,THREAD);if(hasiniterror(THREAD)){setstate(ALNOTINITIALIZED);}KlassgroupSystemDictionary::ThreadGroupklass();JavaValueresult(TVOID);JavaCalls::callspecial(result,threadgroup,group,vmSymbols::addmethodname(),vmSymbols::threadvoidsignature(),threadoop,THREAD);if(hasiniterror(THREAD)){setstate(ALNOTINITIALIZED);}{MutexLockermu(Threadslock);JavaThreadlistenerthreadnewJavaThread(attachlistenerthreadentry);Checkthatthreadandosthreadwerecreatedif(listenerthreadNULLlistenerthreadosthread()NULL){vmexitduringinitialization(java。lang。OutOfMemoryError,os::nativethreadcreationfailedmsg());}javalangThread::setthread(threadoop(),listenerthread);javalangThread::setdaemon(threadoop());listenerthreadsetthreadObj(threadoop());Threads::add(listenerthread);Thread::start(listenerthread);}} 其中不同类型的交互抽象成了AttachOperation,目前已经支持的operation如下。namesmustbeoflengthAttachOperation::namelengthmaxstaticAttachOperationFunctionInfofuncs〔〕{{agentProperties,getagentproperties},{datadump,datadump},{dumpheap,dumpheap},{load,loadagent},{properties,getsystemproperties},{threaddump,threaddump},{inspectheap,heapinspection},{setflag,setflag},{printflag,printflag},{jcmd,jcmd},{NULL,NULL}}; 调用VirtualMachine。load方法会发送一个load类型的AttachOperation,对应的处理函数是loadagentImplementationofloadcommand。staticjintloadagent(AttachOperationop,outputStreamout){getagentnameandoptionsconstcharagentoparg(0);constcharabsParamoparg(1);constcharoptionsoparg(2);Ifloadingajavaagentthenneedtoensurethatthejava。instrumentmoduleisloadedif(strcmp(agent,instrument)0){ThreadTHREADThread::current();ResourceMarkrm(THREAD);HandleMarkhm(THREAD);JavaValueresult(TOBJECT);HandlehmodulenamejavalangString::createfromstr(java。instrument,THREAD);JavaCalls::callstatic(result,SystemDictionary::moduleModulesklass(),vmSymbols::loadModulename(),vmSymbols::loadModulesignature(),hmodulename,THREAD);if(HASPENDINGEXCEPTION){javalangThrowable::print(PENDINGEXCEPTION,out);CLEARPENDINGEXCEPTION;returnJNIERR;}}returnJvmtiExport::loadagentlibrary(agent,absParam,options,out);}ClassFileTransformer是如何注册、调用的ClassFileTransformer注册 Instrumentation。addTransformer会将Transformer保存到TransformerManager类中,按照能否retransform分为两个TransformerManager,每个TransformerManager中通过数组保存Transformer。publicsynchronizedvoidaddTransformer(ClassFileTransformertransformer,booleancanRetransform){if(transformernull){thrownewNullPointerException(nullpassedastransformerinaddTransformer);}if(canRetransform){if(!isRetransformClassesSupported()){thrownewUnsupportedOperationException(addingretransformabletransformersisnotsupportedinthisenvironment);}if(mRetransfomableTransformerManagernull){mRetransfomableTransformerManagernewTransformerManager(true);}mRetransfomableTransformerManager。addTransformer(transformer);if(mRetransfomableTransformerManager。getTransformerCount()1){setHasRetransformableTransformers(mNativeAgent,true);}}else{mTransformerManager。addTransformer(transformer);if(mTransformerManager。getTransformerCount()1){setHasTransformers(mNativeAgent,true);}}}publicsynchronizedvoidaddTransformer(ClassFileTransformertransformer){TransformerInfo〔〕oldListmTransformerLTransformerInfo〔〕newListnewTransformerInfo〔oldList。length1〕;System。arraycopy(oldList,0,newList,0,oldList。length);newList〔oldList。length〕newTransformerInfo(transformer);mTransformerListnewL}ClassFileTransformer调用 那么ClassFileTransformer是如何被调用的呢,以类加载时调用ClassFileTransformer为例。 在jvm加载类时,会回调各个jvmti调用类加载事件回调接口ClassFileLoadHook instrumentjvmti的ClassFileLoadHook实现是调用InstrumentationImpl的transform方法。voidtransformClassFile(JPLISAgentagent,JNIEnvjnienv,jobjectloaderObject,constcharname,jclassclassBeingRedefined,jobjectprotectionDomain,jintclassdatalen,constunsignedcharclassdata,jintnewclassdatalen,unsignedcharnewclassdata,jbooleanisretransformer){。。。省略transformedBufferObject(jnienv)CallObjectMethod(jnienv,agentmInstrumentationImpl,agentmTransform,moduleObject,loaderObject,classNameStringObject,classBeingRedefined,protectionDomain,classFileBufferObject,isretransformer);errorOutstandingcheckForAndClearThrowable(jnienv);jplisassertmsg(!errorOutstanding,transformmethodcallfailed);}if(!errorOutstanding){newclassdatalen(transformedBufferSize);newclassdataresultB}。。。省略}} InstrumentationImpl的transform方法的实现是根据当前是否是retransform来选择TransformerManager,然后调用TransformerManager的transform方法。WARNING:thenativecodeknowsthenamesignatureofthismethodprivatebyte〔〕transform(Modulemodule,ClassLoaderloader,Stringclassname,C?classBeingRedefined,ProtectionDomainprotectionDomain,byte〔〕classfileBuffer,booleanisRetransformer){TransformerManagermgrisRetransformer?mRetransfomableTransformerManager:mTransformerMmoduleisnullwhennotaclassloadorwhenloadingaclassinanunnamedmoduleandthisisthefirsttypetobeloadedinthepackage。if(modulenull){if(classBeingRedefined!null){moduleclassBeingRedefined。getModule();}else{module(loadernull)?jdk。internal。loader。BootLoader。getUnnamedModule():loader。getUnnamedModule();}}if(mgrnull){nomanager,notransform}else{returnmgr。transform(module,loader,classname,classBeingRedefined,protectionDomain,classfileBuffer);}} TransformerManager的transform方法实现逻辑是依次调用Transformer数组中的各个Transformer(就像server中的Filter),然后把最终的bytes结果返回。publicbyte〔〕transform(Modulemodule,ClassLoaderloader,Stringclassname,C?classBeingRedefined,ProtectionDomainprotectionDomain,byte〔〕classfileBuffer){booleansomeoneTouchedTheBTransformerInfo〔〕transformerListgetSnapshotTransformerList();byte〔〕bufferToUseclassfileBordermatters,gottarunemintheordertheywereaddedfor(intx0;xtransformerList。x){TransformerInfotransformerInfotransformerList〔x〕;ClassFileTransformertransformertransformerInfo。transformer();byte〔〕transformedBtry{transformedBytestransformer。transform(module,loader,classname,classBeingRedefined,protectionDomain,bufferToUse);}catch(Throwablet){dontletanyonetransformermessitupfortheothers。Thisiswhereweneedtoputsomelogging。Whatshouldgohere?FIXME}if(transformedBytes!null){someoneTouchedTheBbufferToUsetransformedB}}ifsomeonemodifiedit,returnthemodifiedbuffer。otherwisereturnnulltomeannotransformsoccurredbyte〔〕if(someoneTouchedTheBytecode){resultbufferToU}else{}}总结 本文我们掌握了javaagent的常见应用场景比如分布式tracing、性能分析、在线诊断、热更新等。了解了如何创建一个javaagent来实现AOP功能以及如何使用它。了解了javaagent在启动时加载和运行时加载的两种使用方式,还有通过ByteBuddyAgent。install()的使用方式。了解了VirtualMachine。attach()以及loadAgent是如何通过AttachListener与jvm通信的。了解了jvm中的instrument动态链接库的实现。