Java Agent
前言
当我们使用java命令执行一个java程序时,可以添加一个参数 -javaagent,如下图:
概念
Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。
Java Agent 是在 JDK1.5 引入的一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行。Java Agent在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程进行工作。
功能及应用
功能:
在JVM加载字节码之前拦截并对字节码进行修改;
在JVM 运行期间修改已经加载的字节码;
应用:
IDE 的调试功能,例如 Eclipse、IntelliJ IDEA
热部署功能,例如 JRebel、XRebel、spring-loaded
各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas
各种性能分析工具,例如 Visual VM、JConsole 等
全链路性能检测工具,例如 Skywalking、Pinpoint等
例子
启动时加载
- 新建maven项目,resource目录添加MANIFEST.MF文件,注意最后要多空一行
Premain-Class: org.example.preagent.PreAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
- 定义Agent类,实现premain方法
public class PreAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("pre main 探针启动");
System.out.println("pre main 探针传入参数: " + agentArgs);
instrumentation.addTransformer(new PreTransformer());
}
}
- 定义Transform类,这里通过JavaAssist修改字节码
public class PreTransformer implements ClassFileTransformer {
private static final String INJECTED_CLASS_NAME = "org.example.AppInit";
// className参数表示当前加载类的类名
// classfileBuffer参数是待加载类文件的字节数组
// 调用addTransformer注册ClassFileTransformer以后,后续所有JVM加载类都会被它的transform方法拦截
// 这个方法接收原类文件的字节数组,在这个方法中做类文件改写,最后返回转换过的字节数组,由JVM加载这个修改过的类文件
// 如果transform方法返回null,表示不对此类做处理,如果返回值不为null,JVM会用返回的字节数组替换原来类的字节数组
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
if (realClassName.equals(INJECTED_CLASS_NAME)) {
System.out.println("拦截到的类名: " + className);
CtClass ctClass;
try {
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
System.out.println(method.getName() + " 方法被拦截");
method.addLocalVariable("time", CtClass.longType);
method.insertBefore("System.out.println(\"---开始执行---\");");
method.insertBefore("time = System.currentTimeMillis();");
method.insertAfter("System.out.println(\"---结束执行---\");");
method.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
}
- 修改pom文件打包配置,将项目打包成jar包
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<manifestEntries>
<Premain-Class>org.example.preagent.PreAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
- 编写测试类
//Main方法
public class PreAgentTestMain {
public static void main(String[] args) {
System.out.println("=================main thread start=================");
AppInit.init();
System.out.println("=================main thread end=================");
}
}
//要拦截的类
public class AppInit {
public static void init() {
try {
System.out.println("---App init started---");
Thread.sleep(1000L);
System.out.println("---App init finished---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 在终端或者shell输入javac xxx/PreAgentTestMain.java得到class文件,再通过-javaagent的方式启动java程序
java xxx/PreAgentTest
运行时加载
- 定义Agent
public class RuntimeAgent {
public static void agentmain(String arg, Instrumentation instrumentation) {
System.out.println("runtime agent 探针启动");
System.out.println("runtime agent 探针传入参数:" + arg);
instrumentation.addTransformer(new RuntimeTransformer());
}
}
- 定义Transformer与上面类似
- 打包成jar包
- 测试类启动,注意这里用到了sun的jar包,可以在pom中加入以下依赖
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
public class RuntimeAgentTestMain {
public static void main(String[] args) {
System.out.println("=================main thread start=================");
System.out.println("===APP start===");
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
if (descriptor.displayName().contains("RuntimeAgent")) {
String jvmId = descriptor.id();
System.out.println("当前的JVM:" + jvmId);
try {
VirtualMachine vm = VirtualMachine.attach(jvmId);
vm.loadAgent("/Users/eric/workspace/java-agent/agent-runtime/target/agent-runtime-1.0-SNAPSHOT.jar=\"hello my runtime agent\"");
vm.detach();
} catch (AttachNotSupportedException | IOException | AgentLoadException |
AgentInitializationException e) {
e.printStackTrace();
}
}
}
AppInit.init();
System.out.println("=================main thread start=================");
}
}
- 执行结果
=================main thread start=================
===APP start===
当前的JVM:73833
runtime agent 探针启动
runtime agent 探针传入参数:"hello my runtime agent"
拦截到的类名: org/example/AppInit
init 方法被拦截
---开始执行---
---App init started---
---App init finished---
---结束执行---
运行耗时: 1004
=================main thread start=================
原理
Instrucmentation
JDK定义的一个类,介绍他之前先介绍下JVMTI
JVMTI
JVMTI是java平台调试体系JPDA (Java Platform Debugger Architecture)的一部分。JPDA是Java虚拟机为了调试和监控JVM专门提供的一套接口。这里简单介绍下JDPA:
模块
|
层次
|
作用
|
JDI
|
高
|
提供Java API来远程控制被调试虚拟机
|
JDWP
|
中
|
定义JVMTI和JDI交互的数据格式
|
JVMTI
|
低
|
获取及控制当前虚拟机状态
|
JVMTI 全称是JVM Tool Interface,是 JVM 提供的给用户扩展使用的native编程接口集合,可以使开发者直接与C/C++以及JNI打交道。
如何使用这套接口:一般采用建立一个Agent的方式来使用JVMTI,这个Agent的表现形式是一个以C/C++编写的动态链接库,把Agent编译成一个动态链接库,Java启动或运行时,动态加载一个外部基于JVMTI 编写的dynamic module到Java进程内,然后触发JVM原生线程Attach Listener来执行这个dynamic module的回调函数。在回调函数体内,可以获取各种各样的VM级信息,注册感兴趣的VM事件,甚至控制VM行为。
JVMTI Agent是以动态链接库的形式被虚拟机加载的,区别于普通的动态链接库,一般会实现如下的一个或者多个函数:
Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
Agent_OnUnload函数,在agent卸载时调用
所以Instrumentation就是一个JVMTI Agent。Instrumentation 实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载通过类似
-javaagent:jar包路径的方式来间接加载instrument agent,其中运行时动态加载,使用了JVM attach的方式。JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 命令,然后把 pid 等参数传递给需要 dump 的线程来执行。
启动时加载Agent流程
参数解析
JVM创建时会进行参数解析,这里只关注读取到的JVM命令行参数 -agentlib -agentpath -javaagent,这几个参数用来指定Agent,JVM会根据这几个参数加载Agent构建了Agent Library链表。初始化 Agent 代码如下:https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/arguments.cpp
加载Agent
create_vm_init_agents这个函数通过遍历Agent链表来逐个加载Agent。首先通过lookup_agent_on_load来加载Agent并且找到Agent_OnLoad函数,这个函数是Agent的入口函数。如果没找到这个函数,则认为是加载了一个不合法的Agent,则什么也不做,否则调用这个函数,这样Agent的代码就开始执行起来了。对于使用Java Instrumentation API来编写Agent的方式来说,在解析阶段观察到在add_init_agent函数里面传递进去的是一个叫做"instrument"的字符串,其实这是一个动态链接库。在Linux里面,这个库叫做libinstrument.so,在BSD系统中叫做libinstrument.dylib,该动态链接库在{JAVA_HOME}/jre/lib/目录下。
Instrument动态链接库
libinstrument用来支持使用Java Instrumentation API来编写Agent,在libinstrument 中有一个非常重要的类称为:JPLISAgent(Java Programming Language Instrumentation Services Agent),它的作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任。我们已经知道,在JVM启动的时候,JVM会通过-javaagent参数加载Agent。最开始加载的是libinstrument动态链接库,然后在动态链接库里面找到JVMTI的入口方法:Agent_OnLoad。下面就来分析一下在libinstrument动态链接库中,Agent_OnLoad函数是怎么实现的。
JNIEXPORT jint JNICALL
DEF_Agent_OnLoad(JavaVM * vm , char *tail, void * reserved ) {
initerror = createNewJPLISAgent( vm , &agent);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
if (parseArgumentTail(tail, &jarfile, &options) != 0) {
fprintf( stderr , "-javaagent: memory allocation failure.\n");
return JNI_ERR;
}
attributes = readAttributes(jarfile);
premainClass = getAttribute(attributes, "Premain-Class");
/* Save the jarfile name */
agent->mJarfile = jarfile;
/*
* Convert JAR attributes into agent capabilities
*/
convertCapabilityAttributes(attributes, agent);
/*
* Track (record) the agent class name and options data
*/
initerror = recordCommandLineData(agent, premainClass, options);
}
return result;
}
JNIEXPORT jint JNICALL
DEF_Agent_OnLoad(JavaVM * vm , char *tail, void * reserved ) {
initerror = createNewJPLISAgent( vm , &agent);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
if (parseArgumentTail(tail, &jarfile, &options) != 0) {
fprintf( stderr , "-javaagent: memory allocation failure.\n");
return JNI_ERR;
}
attributes = readAttributes(jarfile);
premainClass = getAttribute(attributes, "Premain-Class");
/* Save the jarfile name */
agent->mJarfile = jarfile;
/*
* Convert JAR attributes into agent capabilities
*/
convertCapabilityAttributes(attributes, agent);
/*
* Track (record) the agent class name and options data
*/
initerror = recordCommandLineData(agent, premainClass, options);
}
return result;
}
上述代码片段是经过精简的libinstrument中Agent_OnLoad实现的,大概的流程就是:先创建一个JPLISAgent,然后将ManiFest中设定的一些参数解析出来, 比如(Premain-Class)等。创建了JPLISAgent之后,调用initializeJPLISAgent对这个Agent进行初始化操作。跟进initializeJPLISAgent看一下是如何初始化的:
JPLISInitializationError initializeJPLISAgent(JPLISAgent *agent, JavaVM *vm, jvmtiEnv *jvmtienv) {
/* check what capabilities are available */
checkCapabilities(agent);
/* check phase - if live phase then we don't need the VMInit event */
jvmtierror = (*jvmtienv)->GetPhase(jvmtienv, &phase);
/* now turn on the VMInit event */
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMInit = &eventHandlerVMInit;
jvmtierror = (*jvmtienv)->SetEventCallbacks(jvmtienv,&callbacks,sizeof(callbacks));
}
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtierror = (*jvmtienv)->SetEventNotificationMode(jvmtienv,JVMTI_ENABLE,JVMTI_EVENT_VM_INIT,NULL);
}
return (jvmtierror == JVMTI_ERROR_NONE)? JPLIS_INIT_ERROR_NONE : JPLIS_INIT_ERROR_FAILURE;
}
其中eventHandlerVMInit:
void JNICALL eventHandlerVMInit( jvmtiEnv *jvmtienv,JNIEnv *jnienv,jthread thread) {
// ...
success = processJavaStart( environment->mAgent, jnienv);
// ...
}
jboolean processJavaStart(JPLISAgent *agent,JNIEnv *jnienv) {
result = createInstrumentationImpl(jnienv, agent);
/*
* Load the Java agent, and call the premain.
*/
if ( result ) {
result = startJavaAgent(agent, jnienv, agent->mAgentClassName, agent->mOptionsString, agent->mPremainCaller);
}
return result;
}
jboolean startJavaAgent( JPLISAgent *agent,JNIEnv *jnienv,const char *classname,const char *optionsString,jmethodID agentMainMethod) {
// ...
invokeJavaAgentMainMethod (jnienv,agent->mInstrumentationImpl,agentMainMethod, classNameObject,optionsStringObject);
// ...
}
JPLISInitializationError initializeJPLISAgent(JPLISAgent *agent, JavaVM *vm, jvmtiEnv *jvmtienv) {
/* check what capabilities are available */
checkCapabilities(agent);
/* check phase - if live phase then we don't need the VMInit event */
jvmtierror = (*jvmtienv)->GetPhase(jvmtienv, &phase);
/* now turn on the VMInit event */
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMInit = &eventHandlerVMInit;
jvmtierror = (*jvmtienv)->SetEventCallbacks(jvmtienv,&callbacks,sizeof(callbacks));
}
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtierror = (*jvmtienv)->SetEventNotificationMode(jvmtienv,JVMTI_ENABLE,JVMTI_EVENT_VM_INIT,NULL);
}
return (jvmtierror == JVMTI_ERROR_NONE)? JPLIS_INIT_ERROR_NONE : JPLIS_INIT_ERROR_FAILURE;
}
其中eventHandlerVMInit:
void JNICALL eventHandlerVMInit( jvmtiEnv *jvmtienv,JNIEnv *jnienv,jthread thread) {
// ...
success = processJavaStart( environment->mAgent, jnienv);
// ...
}
jboolean processJavaStart(JPLISAgent *agent,JNIEnv *jnienv) {
result = createInstrumentationImpl(jnienv, agent);
/*
* Load the Java agent, and call the premain.
*/
if ( result ) {
result = startJavaAgent(agent, jnienv, agent->mAgentClassName, agent->mOptionsString, agent->mPremainCaller);
}
return result;
}
jboolean startJavaAgent( JPLISAgent *agent,JNIEnv *jnienv,const char *classname,const char *optionsString,jmethodID agentMainMethod) {
// ...
invokeJavaAgentMainMethod (jnienv,agent->mInstrumentationImpl,agentMainMethod, classNameObject,optionsStringObject);
// ...
}
看到这里,Instrument已经实例化,invokeJavaAgentMainMethod这个方法将我们的Premain方法执行起来了。接着,我们就可以根据Instrument实例来做我们想要做的事情了
总结一下就是:
Instrument是Java给我们提供的一种JVMTI Agent;
libinstrument中有个类JPLISAgent初始化时会加载我们通过Instrument的编写的Agent实例,同时注册一个VMInit事件的回调函数;
VM初始化的时候,发送事件,通过invoke执行我们定义的premain方法;
运行时加载Agent流程
运行时动态加载agent,使用了JVM attach的方式。JVM Attach是指 JVM 提供的一种进程间通信的功能,可以让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump:执行 jstack 命令,然后把 pid 等参数传递给需要 dump 的线程来执行。
Attach机制
Attach机制的主要功能就是实现了一个JVM进程和另一个JVM进程之前相互发送命令进行通信的机制。Attach机制可以对目标进程收集很多信息,如内存dump,线程dump,类信息统计(比如加载的类及大小以及实例个数等),动态加载agent,动态设置vm flag,打印vm flag,获取系统属性等等,这些对应的源码(AttachListener.cpp)
Attach机制实现的关键是两个JVM线程,分别是Attach Listener线程和Signal Dipatcher线程。
Attach Listener线程负责接收外部发送来的命令,并处理JVM命令返回结果
Signal Dispatcher线程负责将信号分发,然后将结果返回。
Attach Listener线程并非是随着JVM启动而启动的,而是需要在启动JVM时启动,或者当第一个JVM命令到来时才启动;Signal Dispatcher线程是随着JVM启动。
Attach流程
当外部进程Attach目标JVM时,会向目标进程发送sigquit信号,目标进程接收到信号之后广播给子线程,Signal Dispatcher线程会处理该信号,并会创建Attach Listener线程
Attach Listener线程启动之后,会创建监听套接字文件/tmp/.java_pid,表示外部进程Attach目标JVM成功。之后外部进程发送命令写入该套接字,Attach Listener线程监听该套接字,解析成功命令进行处理。
Attach Listener线程命令对应的源码(AttachListener.cpp)如下:

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人