本文重点讲述javaagent的具体实现,由于它面向的是咱们java程序员,并且agent都是用java编写的,不须要太多的c/c++编程基础,不过这篇文章里也会讲到JVMTIAgent(c实现的),由于javaagent的运行仍是依赖于一个特殊的JVMTIAgent。java
对于javaagent或许你们都听过,甚至使用过,常见的用法大体以下:linux
java -javaagent:myagent.jar=mode=test Test
咱们经过-javaagent来指定咱们编写的agent的jar路径(./myagent.jar)及要传给agent的参数(mode=test),这样在启动的时候这个agent就能够作一些咱们想要它作的事了。c++
javaagent的主要的功能以下:程序员
还有其余的一些小众的功能编程
JVMTI全称JVM Tool Interface,是jvm暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到必定的逻辑就会调用一些事件的回调接口(若是有的话),这些接口能够供开发者去扩展本身的逻辑。bootstrap
好比说咱们最多见的想在某个类的字节码文件读取以后类定义以前能修改相关的字节码,从而使建立的class对象是咱们修改以后的字节码内容,那咱们就能够实现一个回调函数赋给JvmtiEnv(JVMTI的运行时,一般一个JVMTIAgent对应一个jvmtiEnv,可是也能够对应多个)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程当中都会调用到这个函数里来了,大体实现以下:缓存
jvmtiEventCallbacks callbacks; jvmtiEnv * jvmtienv = jvmti(agent); jvmtiError jvmtierror; memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook; jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks));
JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些咱们想作可是正常状况下又作不到的事情,不过为了和普通的动态库进行区分,它通常会实现以下的一个或者多个函数:数据结构
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved); JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved); JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm);
Agent_OnLoad
函数,若是agent是在启动的时候加载的,也就是在vm参数里经过-agentlib来指定,那在启动过程当中就会去执行这个agent里的Agent_OnLoad
函数。Agent_OnAttach
函数,若是agent不是在启动的时候加载的,是咱们先attach到目标进程上,而后给对应的目标进程发送load命令来加载agent,在加载过程当中就会调用Agent_OnAttach
函数。Agent_OnUnload
函数,在agent作卸载的时候调用,不过貌似基本上不多实现它。其实咱们天天都在和JVMTIAgent打交道,只是你可能没有意识到而已,好比咱们常用eclipse等工具对java代码作调试,其实就利用了jre自带的jdwp agent来实现的,只是因为eclipse等工具在没让你察觉的状况下将相关参数(相似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349
)给自动加到程序启动参数列表里了,其中agentlib参数就是用来跟要加载的agent的名字,好比这里的jdwp(不过这不是动态库的名字,而JVM是会作一些名称上的扩展,好比在linux下会去找libjdwp.so
的动态库进行加载,也就是在名字的基础上加前缀lib
,再加后缀.so
),接下来会跟一堆相关的参数,会将这些参数传给Agent_OnLoad
或者Agent_OnAttach
函数里对应的options
参数。app
说到javaagent必需要讲的是一个叫作instrument的JVMTIAgent(linux下对应的动态库是libinstrument.so),由于就是它来实现javaagent的功能的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),从这名字里也彻底体现了其最本质的功能:就是专门为java语言编写的插桩服务提供支持的。eclipse
instrument agent实现了Agent_OnLoad
和Agent_OnAttach
两方法,也就是说咱们在用它的时候既支持启动的时候来加载agent,也支持在运行期来动态来加载这个agent,其中启动时加载agent还能够经过相似-javaagent:myagent.jar
的方式来间接加载instrument agent,运行期动态加载agent依赖的是jvm的attach机制JVM Attach机制实现,经过发送load命令来加载agent。
instrument agent的核心数据结构以下:
struct _JPLISAgent { JavaVM * mJVM; /* handle to the JVM */ JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */ JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */ jobject mInstrumentationImpl; /* handle to the Instrumentation instance */ jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */ jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */ jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */ jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */ jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */ jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */ jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */ char const * mAgentClassName; /* agent class name */ char const * mOptionsString; /* -javaagent options string */ }; struct _JPLISEnvironment { jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */ JPLISAgent * mAgent; /* corresponding agent */ jboolean mIsRetransformer; /* indicates if special environment */ };
这里解释下几个重要项:
premain
以及agentmain
方法的时候注意到了有个Instrumentation的参数,这个参数其实就是这里的对象。sun.instrument.InstrumentationImpl.loadClassAndCallPremain
方法,若是agent是在启动的时候加载的,那该方法会被调用。sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain
方法,该方法在经过attach的方式动态加载agent的时候调用。sun.instrument.InstrumentationImpl.transform
方法。Agent-Class
。Can-Redefine-Classes:true
。Can-Set-Native-Method-Prefix:true
。正如『概述』里提到的方式,就是启动的时候加载instrument agent,具体过程都在InvocationAdapter.c
的Agent_OnLoad
方法里,简单描述下过程:
监听VMInit事件,在vm初始化完成以后作下面的事情:
运行时加载的方式,大体按照下面的方式来操做:
VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath, agentArgs);
上面会经过jvm的attach机制来请求目标jvm加载对应的agent,过程大体以下:
loadClassAndCallAgentmain
方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Agent-Class
类的agentmain
方法不论是启动时仍是运行时加载的instrument agent都关注着同一个jvmti事件—-ClassFileLoadHook
,这个事件是在读取字节码文件以后回调时用的,这样能够对原来的字节码作修改,那这里面到底是怎样实现的呢?
void JNICALL eventHandlerClassFileLoadHook( jvmtiEnv * jvmtienv, JNIEnv * jnienv, jclass class_being_redefined, jobject loader, const char* name, jobject protectionDomain, jint class_data_len, const unsigned char* class_data, jint* new_class_data_len, unsigned char** new_class_data) { JPLISEnvironment * environment = NULL; environment = getJPLISEnvironment(jvmtienv); /* if something is internally inconsistent (no agent), just silently return without touching the buffer */ if ( environment != NULL ) { jthrowable outstandingException = preserveThrowable(jnienv); transformClassFile( environment->mAgent, jnienv, loader, name, class_being_redefined, protectionDomain, class_data_len, class_data, new_class_data_len, new_class_data, environment->mIsRetransformer); restoreThrowable(jnienv, outstandingException); } }
先根据jvmtiEnv取得对应的JPLISEnvironment,由于上面我已经说到其实有两个JPLISEnvironment(而且有两个jvmtiEnv),其中一个专门作retransform的,而另一个用来作其余的事情,根据不一样的用途咱们在注册具体的ClassFileTransformer的时候也是分开的,对于做为retransform用的ClassFileTransformer咱们会注册到一个单独的TransformerManager里。
接着调用transformClassFile方法,因为函数实现比较长,我这里就不贴代码了,大体意思就是调用InstrumentationImpl对象的transform方法,根据最后那个参数来决定选哪一个TransformerManager里的ClassFileTransformer对象们作transform操做。
以上是最终调到的java代码,能够看到已经调用到咱们本身编写的javaagent代码里了,咱们通常是实现一个ClassFileTransformer类,而后建立一个对象注册了对应的TransformerManager里。
这里说的class transform实际上是狭义的,主要是针对第一次类文件加载的时候就要求被transform的场景,在加载类文件的时候发出ClassFileLoad的事件,而后交给instrumenat agent来调用javaagent里注册的ClassFileTransformer实现字节码的修改。
类从新定义,这是Instrumentation提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,要作这件事,咱们必需要知道两个东西,一个是要修改哪一个类,另一个是那个类你想修改为怎样的结构,有了这两信息以后因而你就能够经过InstrumentationImpl的下面的redefineClasses方法去操做了:
public void redefineClasses(ClassDefinition[] definitions) throws ClassNotFoundException { if (!isRedefineClassesSupported()) { throw new UnsupportedOperationException("redefineClasses is not supported in this environment"); } if (definitions == null) { throw new NullPointerException("null passed as 'definitions' in redefineClasses"); } for (int i = 0; i < definitions.length; ++i) { if (definitions[i] == null) { throw new NullPointerException("element of 'definitions' is null in redefineClasses"); } } if (definitions.length == 0) { return; // short-circuit if there are no changes requested } redefineClasses0(mNativeAgent, definitions); }
在JVM里对应的实现是建立一个VM_RedefineClasses的VM_Operation,注意执行它的时候会stop the world的:
jvmtiError JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) { //TODO: add locking VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine); VMThread::execute(&op); return (op.check_error()); } /* end RedefineClasses */
这个过程我尽可能用语言来描述清楚,不详细贴代码了,由于代码量实在有点大:
对比新老类,并要求以下:
上面是基本的过程,总的来讲就是只更新了类里内容,至关于只更新了指针指向的内容,并无更新指针,避免了遍历大量已有类对象对它们进行更新带来的开销。
retransform class能够简单理解为回滚操做,具体回滚到哪一个版本,这个须要看状况而定,下面无论那种状况都有一个前提,那就是javaagent已经要求要有retransform的能力了:
若是类是在第一次加载的的时候就作了transform,那么作retransform的时候会将代码回滚到transform以后的代码
若是类是在第一次加载的的时候没有任何变化,那么作retransform的时候会将代码回滚到最原始的类文件里的字节码
若是类已经被加载了,期间类可能作过屡次redefine(好比被另一个agent作过),可是接下来加载一个新的agent要求有retransform的能力了,而后对类作redefine的动做,那么retransform的时候会将代码回滚到上一个agent最后一次作redefine后的字节码
咱们从InstrumentationImpl的retransformClasses方法参数看猜到应该是作回滚操做,由于咱们只指定了class
public void retransformClasses(Class<?>[] classes) { if (!isRetransformClassesSupported()) { throw new UnsupportedOperationException( "retransformClasses is not supported in this environment"); } retransformClasses0(mNativeAgent, classes); }
不过retransform的实现其实也是经过redefine的功能来实现,在类加载的时候有比较小的差异,主要体如今究竟会走哪些transform上,若是当前是作retransform的话,那将忽略那些注册到正常的TransformerManager里的ClassFileTransformer,而只会走专门为retransform而准备的TransformerManager的ClassFileTransformer,否则想象一下字节码又被无声无息改为某个中间态了。
private: void post_all_envs() { if (_load_kind != jvmti_class_load_kind_retransform) { // for class load and redefine, // call the non-retransformable agents JvmtiEnvIterator it; for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) { if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) { // non-retransformable agents cannot retransform back, // so no need to cache the original class file bytes post_to_env(env, false); } } } JvmtiEnvIterator it; for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) { // retransformable agents get all events if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) { // retransformable agents need to cache the original class file // bytes if changes are made via the ClassFileLoadHook post_to_env(env, true); } } }
javaagent除了作字节码上面的修改以外,其实还有一些小功能,有时候仍是挺有用的
Class[] getAllLoadedClasses();
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);