java agent实现原理总结~~~
前言:
我们对javaAgent技术的基本使用有了一个初步的认识,但是只学使用不学技术,不是程序员应该有的风格特点,所以接下来需要探究一下javaAgent技术的实现原理,涉及到JVM底层内容;
java agent工作流程大致如下:

有上图可知,agent实现有两种方式:java agent和JVMTI agent方式,但是这两种方式都依赖于JVMTI;
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。
Java agent与Instrumentation密不可分,二者也需要在一起使用。因为Instrumentation的实例会作为参数注入到Java agent的启动方法中。
JVMTI (JVM Tool Interface)是 Java 虚拟机对外提供的 Native 编程接口,通过 JVMTI ,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。JVMTI 是一套 Native 接口,在 Java SE 5 之前,要实现一个 Agent 只能通过编写 Native 代码来实现。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。
逻辑介绍
1)Instrumentation API 介绍
Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。
Instrumentation API的一些主要方法:
public interface Instrumentation {/*** 注册一个Transformer,从此之后的类加载都会被Transformer拦截。* Transformer可以直接对类的字节码byte[]进行修改*/void addTransformer(ClassFileTransformer transformer);/*** 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。* retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性*/void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;/*** 获取一个对象的大小*/long getObjectSize(Object objectToSize);/*** 将一个jar加入到bootstrap classloader的 classpath里*/void appendToBootstrapClassLoaderSearch(JarFile jarfile);/*** 获取当前被JVM加载的所有类对象*/Class[] getAllLoadedClasses();
}
其中最常用的方法是addTransformer(ClassFileTransformer transformer),这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:
public interface ClassFileTransformer {/*** 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]* 返回值为需要被修改后的字节码byte[]*/byte[]transform( ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer)throws IllegalClassFormatException;
}
addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
java agent的加载过程
1)静态加载过程
agent 中的 class 由 system calss loader(默认AppClassLoader) 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发ClassFileTransformer.transform(),最终修改 class 的字节码。

Instrument agent启动时加载会实现Agent_OnLoad方法,具体实现逻辑如下:
1.创建并初始化JPLISAgent
2.监听VMInit事件,在vm初始化完成之后执行下面逻辑
a.创建Instrumentation接口的实例,也就是InstrumentationImpl对象
b.监听ClassFileLoadHook事件(类加载事件)
c.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
3.解析MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的内容
ClassFileTransformer.transform() 和 ClassLoader.load()的关系
下面是一次 ClassFileTransformer.transform()执行时的方法调用栈,
transform:38, MethodAgentMain$1 (demo)
transform:188, TransformerManager (sun.instrument)
transform:428, InstrumentationImpl (sun.instrument)
defineClass1:-1, ClassLoader (java.lang)
defineClass:760, ClassLoader (java.lang)
defineClass:142, SecureClassLoader (java.security)
defineClass:467, URLClassLoader (java.net)
access$100:73, URLClassLoader (java.net)
run:368, URLClassLoader$1 (java.net)
run:362, URLClassLoader$1 (java.net)
doPrivileged:-1, AccessController (java.security)
findClass:361, URLClassLoader (java.net)
loadClass:424, ClassLoader (java.lang)
loadClass:331, Launcher$AppClassLoader (sun.misc)
loadClass:357, ClassLoader (java.lang)
checkAndLoadMain:495, LauncherHelper (sun.launcher)
可以看到 ClassLoader.load()加载类时,ClassLoader.load()会调用ClassLoader.findClass(),ClassLoader.findClass()会调用ClassLoader.defefineClass(),ClassLoader.defefineClass()最终会执行ClassFileTransformer.transform() ,ClassFileTransformer.transform() 可以对类进行修改。所以ClassLoader.load()最终加载 agent 修改后Class对象。
下面是精简后的 ClassLoader.load() 核心代码:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {// 判断是否已经加载过了,如果没有,则进行load// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();// findClass()内部最终会调用 Java agent 中 ClassFileTransformer.transform()c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
ClassFileTransformer.transform() 和 字节码增强ClassFileTransformer.transform() 中可以对指定的类进行增强,我们可以选择的代码生成库修改字节码对类进行增强,比如ASM, CGLIB, Byte Buddy, Javassist。
2)动态加载 Java agent
对于VM启动后加载的Java agent,其agentmain()方法会在加载之时立即执行。如果agentmain执行失败或抛出异常,JVM会忽略掉错误,不会影响到正在 running 的 Java 程序。
一般 agentmain() 中会编写如下步骤:
a、注册类的 ClassFileTransformer
b、调用 retransformClasses 方法对指定的类进行重加载
Instrument agent运行时加载会使用Agent_OnAttach方法,会通过JVM的attach机制来请求目标JVM加载对应的agent,过程如下:
1.创建并初始化JPLISAgent
2.解析javaagent里的MANIFEST.MF里的参数
3.创建InstrumentationImpl对象
4.监听ClassFileLoadHook事件
5.调用InstrumentationImpl类的loadClassAndCallPremain方法,这个方法会调用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class类的premain方法
JVMTIAgent介绍:
JVMTI 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现;
JVMTIAgent是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
Agent_OnUnload函数,在agent卸载时调用
javaagent 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的
JPLISAgent结构:
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 */
};
属性说明:
mNormalEnvironment:agent环境;
mRetransformEnvironment:retransform环境;
mInstrumentationImpl:sun自己提供的instrument对象;
mPremainCaller:sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,agent启动时加载会被调用该方法;
mAgentmainCaller:sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,agent attach动态加载agent的时会被调用该方法;
mTransform:sun.instrument.InstrumentationImpl.transform方法;
mAgentClassName:javaagent的MANIFEST.MF里指定的Agent-Class;
mOptionsString:agent初始参数;
mRedefineAvailable:MANIFEST.MF里的参数Can-Redefine-Classes:true;
mNativeMethodPrefixAvailable:MANIFEST.MF里的参数Can-Set-Native-Method-Prefix:true;
mIsRetransformer:MANIFEST.MF里的参数Can-Retransform-Classes:true;
instrument 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent
三个函数:
Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数(通过-agentlib加载vm参数中)
Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法
Agent_OnUnload方法:在agent卸载时调用
JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行
ClassFileLoadHook回调实现:
启动时加载和运行时加载都是监听同一个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);}}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
