
java agent实现原理总结~~~
java agent实现原理总结,设计JVMTI、JVMTI Agent、Instrumentation API 和监听 ClassFileLoadHook 事件等
前言:
我们对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 loaded
Class<?> 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 stats
sun.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_OnAttac
h函数,如果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);
}
}
更多推荐
所有评论(0)