javaagent 使用指南
今天打算写一下 Javaagent,一开始我对它的概念也比较陌生,后来在别人口中听到 字节码插桩
,bTrace
,Arthas
后面才逐渐了解到 Java 还提供了这么个工具。
JVM 启动前静态 Instrument#
Javaagent 是什么?
Javaagent 是 java 命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有 2 个要求:
- 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain() 方法。
premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当 Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent
所指定 jar 包内 Premain-Class 这个类的 premain 方法。
在命令行输入 java
可以看到相应的参数,其中有 和 java agent 相关的:
`-agentlib:<libname>[=< 选项 >] 加载本机代理库 <libname>, 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=< 选项 >]
按完整路径名加载本机代理库
-javaagent:<jarpath>[=< 选项 >]
加载 Java 编程语言代理, 请参阅 java.lang.instrument`
在上面 -javaagent
参数中提到了参阅java.lang.instrument
,这是在rt.jar
中定义的一个包,该路径下有两个重要的类:
该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个 Class 类型的转换器,他可以在运行时接受重新外部请求,对 Class 类型进行修改。
从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。上面说到 javaagent 命令要求指定的类中必须要有 premain()方法,并且对 premain 方法的签名也有要求,签名必须满足以下两种格式:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 会优先加载 带 Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在 sun.instrument.InstrumentationImpl 类中:
Instrumentation 类 定义如下:
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
// 在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。void addTransformer(ClassFileTransformer transformer);
// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
// 在类加载之后,重新定义 Class。这个很重要,该方法是 1.6 之后加入的,事实上,该方法是 update 了一个类。void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
// 获取一个对象的大小
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
最为重要的是上面注释的几个方法,下面我们会用到。
如何使用 javaagent?
使用 javaagent 需要几个步骤:
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入 Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
- 创建一个 Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -javaagent: jar 包路径 启动要代理的方法。
在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如 ASM,javassist,cglib 等等来改写实现类。
通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程,一个工程是用来承载 javaagent 类,单独的打成 jar 包;一个工程是 javaagent 需要去代理的类。即 javaagent 会在这个工程中的 main 方法启动之前去做一些事情。
1. 首先来实现 javaagent 工程。
工程目录结构如下:
`-java-agent
—-src
——–main
——–|——java
——–|———-com.rickiyang.learn
——–|————PreMainTraceAgent
——–|resources
———–META-INF
————–MANIFEST.MF`
第一步是需要创建一个类,包含 premain 方法:
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author: rickiyang
* @date: 2019/8/12
* @description:
*/
public class PreMainTraceAgent {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("agentArgs :" + agentArgs);
inst.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
上面就是我实现的一个类,实现了带 Instrumentation 参数的 premain()方法。调用 addTransformer()方法对启动时所有的类进行拦截。
然后在 resources 目录下新建目录:META-INF,在该目录下新建文件:MANIFREST.MF:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent
注意到第 5 行有空行。
说一下 MANIFREST.MF 文件的作用,这里如果你不去手动指定的话,直接 打包,默认会在打包的文件中生成一个 MANIFREST.MF 文件:
Manifest-Version: 1.0
Implementation-Title: test-agent
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: yangyue
Implementation-Vendor-Id: com.rickiyang.learn
Spring-Boot-Version: 2.0.9.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rickiyang.learn.LearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_151
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/test-agent
这是默认的文件,包含当前的一些版本信息,当前工程的启动类,它还有别的参数允许你做更多的事情,可以用上的有:
Premain-Class:包含 premain 方法的类(类的全路径名)
Agent-Class:包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path:设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes:true 表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes:true 表示能重转换此代理所需的类,默认值为 false(可选)
Can-Set-Native-Method-Prefix:true 表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
即在该文件中主要定义了程序运行相关的配置信息,程序运行前会先检测该文件中的配置项。
一个 java 程序中 -javaagent
参数的个数是没有限制的,所以可以添加任意多个 javaagent。所有的 java agent 会按照你定义的顺序执行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
程序执行的顺序将会是:
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
说回上面的 javaagent 工程,接下来将该工程打成 jar 包,我在打包的时候发现打完包之后的 MANIFREST.MF 文件被默认配置替换掉了。所以我是手动将上面我的配置文件替换到 jar 包中的文件,这里你需要注意。
另外的再说一种不去手动写 MANIFREST.MF 文件的方式,使用 maven 插件:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!-- 自动添加 META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class>
<Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
用这种插件的方式也可以自动生成该文件。
agent 代码就写完了,下面再重新开一个工程,你只需要写一个带 main 方法的类即可:
public class TestMain {public static void main(String[] args) {System.out.println("main start");
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("main end");
}
}
很简单,然后需要做的就是将上面的 代理类 和 这个测试类关联起来。有两种方式:
如果你用的是 idea,那么你可以点击菜单:run-debug configuration,然后将你的代理类包 指定在 启动参数中即可:
另一种方式是不用 编译器,采用命令行的方法。与上面大致相同,将 上面的测试类编译成 class 文件,然后 运行该类即可:
`# 将该类编译成 class 文件
javac TestMain.java
#指定 agent 程序并运行该类
java -javaagent:c:/alg.jar TestMain`
使用上面两种方式都可以运行, 输出结果如下:
D:\soft\jdk1.8\bin\java.exe -javaagent:c:/alg.jar "-javaagent:D:\soft\IntelliJ IDEA 2019.1.1\lib\idea_rt.jar=54274:D:\soft\IntelliJ IDEA 2019.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\soft\jdk1.8\jre\lib\charsets.jar;D:\soft\jdk1.8\jre\lib\deploy.jar;D:\soft\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\soft\jdk1.8\jre\lib\ext\cldrdata.jar;D:\soft\jdk1.8\jre\lib\ext\dnsns.jar;D:\soft\jdk1.8\jre\lib\ext\jaccess.jar;D:\soft\jdk1.8\jre\lib\ext\jfxrt.jar;D:\soft\jdk1.8\jre\lib\ext\localedata.jar;D:\soft\jdk1.8\jre\lib\ext\nashorn.jar;D:\soft\jdk1.8\jre\lib\ext\sunec.jar;D:\soft\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\soft\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\soft\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\soft\jdk1.8\jre\lib\ext\zipfs.jar;D:\soft\jdk1.8\jre\lib\javaws.jar;D:\soft\jdk1.8\jre\lib\jce.jar;D:\soft\jdk1.8\jre\lib\jfr.jar;D:\soft\jdk1.8\jre\lib\jfxswt.jar;D:\soft\jdk1.8\jre\lib\jsse.jar;D:\soft\jdk1.8\jre\lib\management-agent.jar;D:\soft\jdk1.8\jre\lib\plugin.jar;D:\soft\jdk1.8\jre\lib\resources.jar;D:\soft\jdk1.8\jre\lib\rt.jar;D:\workspace\demo1\target\classes;E:\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.1.1.RELEASE\spring-
...
...
...
1.8.11.jar;E:\.m2\repository\com\google\guava\guava\20.0\guava-20.0.jar;E:\.m2\repository\org\apache\commons\commons-lang3\3.7\commons-lang3-3.7.jar;E:\.m2\repository\com\alibaba\fastjson\1.2.54\fastjson-1.2.54.jar;E:\.m2\repository\org\springframework\boot\spring-boot\2.1.0.RELEASE\spring-boot-2.1.0.RELEASE.jar;E:\.m2\repository\org\springframework\spring-context\5.1.3.RELEASE\spring-context-5.1.3.RELEASE.jar com.springboot.example.demo.service.TestMain
agentArgs : null
premain load Class :java/util/concurrent/ConcurrentHashMap$ForwardingNode
premain load Class :sun/nio/cs/ThreadLocalCoders
premain load Class :sun/nio/cs/ThreadLocalCoders$1
premain load Class :sun/nio/cs/ThreadLocalCoders$Cache
premain load Class :sun/nio/cs/ThreadLocalCoders$2
premain load Class :java/util/jar/Attributes
premain load Class :java/util/jar/Manifest$FastInputStream
...
...
...
premain load Class :java/lang/Class$MethodArray
premain load Class :java/lang/Void
main start
premain load Class :sun/misc/VMSupport
premain load Class :java/util/Hashtable$KeySet
premain load Class :sun/nio/cs/ISO_8859_1$Encoder
premain load Class :sun/nio/cs/Surrogate$Parser
premain load Class :sun/nio/cs/Surrogate
...
...
...
premain load Class :sun/util/locale/provider/LocaleResources$ResourceReference
main end
premain load Class :java/lang/Shutdown
premain load Class :java/lang/Shutdown$Lock
Process finished with exit code 0
上面的输出结果我们能够发现:
- 执行 main 方法之前会加载所有的类,包括系统类和自定义类;
- 在 ClassFileTransformer 中会去拦截系统类和自己实现的类对象;
- 如果你有对某些类对象进行改写,那么在拦截的时候抓住该类使用字节码编译工具即可实现。
下面是使用 javassist 来动态将某个方法替换掉:
package com.rickiyang.learn;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* @author rickiyang
* @date 2019-08-06
* @Desc
*/
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
// 操作 Date 类
if ("java/util/Date".equals(className)) {
try {
// 从 ClassPool 获得 CtClass 对象
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("java.util.Date");
CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
// 这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return 之前增加了一个 打印操作
String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
"sb.append(name.charAt(1)).append(name.charAt(2));" +
"System.out.println(\"sb.toString()\");" +
"return sb;}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且 detachCtClass 对象
byte[] byteCode = clazz.toBytecode();
//detach 的意思是将内存中曾经被 javassist 加载过的 Date 对象移除,如果下次有需要在内存中找不到会重新走 javassist 加载
clazz.detach();
return byteCode;
} catch (Exception ex) {ex.printStackTrace();
}
}
// 如果返回 null 则字节码不会被修改
return null;
}
}
JVM 启动后动态 Instrument#
上面介绍的 Instrumentation 是在 JDK 1.5 中提供的,开发者只能在 main 加载之前添加手脚,在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。
跟 premain
函数一样,开发者可以编写一个含有 agentmain
函数的 Java 类:
// 采用 attach 机制,被代理的目标程序 VM 有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助 Instrumentation#retransformClasses(Class<?>… classes)让对应的类可以重新转换,从而激活重新转换的类执行 ClassFileTransformer 列表中的回调
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同样,agentmain 方法中带 Instrumentation 参数的方法也比不带优先级更高。开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。
在 Java6 以后实现启动后加载的新实现是 Attach api。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面:
-
VirtualMachine
字面意义表示一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存 dump、线程 dump,类信息统计(比如已加载的类以及实例个数等),loadAgent,Attach 和 Detach(Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大。该类允许我们通过给 attach 方法传入一个 jvm 的 pid(进程 id),远程连接到 jvm 上。代理类注入操作只是它众多功能中的一个,通过
loadAgent
方法向 jvm 注册一个代理程序 agent,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以 在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。在调用 Instrumentation 实例的方法时,这些方法会使用 ClassFileTransformer 接口中提供的方法进行处理。 -
VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
attach 实现动态注入的原理如下:
通过 VirtualMachine 类的 attach(pid)
方法,便可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath)
来将 agent 的 jar 包注入到对应的进程,然后对应的进程会调用 agentmain 方法。
既然是两个进程之间通信那肯定的建立起连接,VirtualMachine.attach 动作类似 TCP 创建连接的三次握手,目的就是搭建 attach 通信的连接。而后面执行的操作,例如 vm.loadAgent,其实就是向这个 socket 写入数据流,接收方 target VM 会针对不同的传入数据来做不同的处理。
我们来测试一下 agentmain 的使用:
工程结构和 上面 premain 的测试一样,编写 AgentMainTest,然后使用 maven 插件打包 生成 MANIFEST.MF。
package com.rickiyang.learn;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author rickiyang
* @date 2019-08-16
* @Desc
*/
public class AgentMainTest {public static void agentmain(String agentArgs, Instrumentation instrumentation) {instrumentation.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!-- 自动添加 META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>com.rickiyang.learn.AgentMainTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
将 agent 打包之后,就是编写测试 main 方法。上面我们画的图中的步骤是:从一个 attach JVM 去探测目标 JVM,如果目标 JVM 存在则向它发送 agent.jar。我测试写的简单了些,找到当前 JVM 并加载 agent.jar。
package com.rickiyang.learn.job;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
/**
* @author rickiyang
* @date 2019-08-16
* @Desc
*/
public class TestAgentMain {public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
// 获取当前系统中所有 运行中的 虚拟机
System.out.println("running JVM start");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
// 如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
// 然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) {VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
virtualMachine.detach();}
}
}
}
list()方法会去寻找当前系统中所有运行着的 JVM 进程,你可以打印 vmd.displayName()
看到当前系统都有哪些 JVM 进程在运行。因为 main 函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程 id。
注意:在 mac 上安装了的 jdk 是能直接找到 VirtualMachine 类的,但是在 windows 中安装的 jdk 无法找到,如果你遇到这种情况,请手动将你 jdk 安装目录下:lib 目录中的 tools.jar 添加进当前工程的 Libraries 中。
运行 main 方法的输出为:
可以看到实际上是启动了一个 socket 进程去传输 agent.jar。先打印了“running JVM start”表名 main 方法是先启动了,然后才进入代理类的 transform 方法。
instrument 原理 #
instrument
的底层实现依赖于 JVMTI(JVM Tool Interface)
,它是 JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。JVMTIAgent
是一个利用 JVMTI
暴露出来的接口提供了代理启动时加载 (agent on load)、代理通过 attach 形式加载(agent on attach) 和代理卸载 (agent on unload) 功能的动态库。而 instrument agent
可以理解为一类 JVMTIAgent
动态库,别名是 JPLISAgent(Java Programming Language Instrumentation Services Agent)
,也就是 专门为 java 语言编写的插桩服务提供支持的代理。
启动时加载 instrument agent 过程:
- 创建并初始化 JPLISAgent;
-
监听
VMInit
事件,在 JVM 初始化完成之后做下面的事情:- 创建 InstrumentationImpl 对象;
- 监听 ClassFileLoadHook 事件;
- 调用 InstrumentationImpl 的
loadClassAndCallPremain
方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的 Premain-Class 类的 premain 方法;
- 解析 javaagent 中 MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容。
运行时加载 instrument agent 过程:
通过 JVM 的 attach 机制来请求目标 JVM 加载对应的 agent,过程大致如下:
- 创建并初始化 JPLISAgent;
- 解析 javaagent 里 MANIFEST.MF 里的参数;
- 创建 InstrumentationImpl 对象;
- 监听 ClassFileLoadHook 事件;
- 调用 InstrumentationImpl 的
loadClassAndCallAgentmain
方法,在这个方法里会去调用 javaagent 里 MANIFEST.MF 里指定的Agent-Class
类的agentmain
方法。
Instrumentation 的局限性 #
大多数情况下,我们使用 Instrumentation 都是使用其字节码插桩的功能,或者笼统说就是类重定义 (Class Redefine) 的功能,但是有以下的局限性:
- premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有 Class 类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
-
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses() 方法,此方法有以下限制:
- 新类和老类的父类必须相同;
- 新类和老类实现的接口数也要相同,并且是相同的接口;
- 新类和老类访问符必须一致。新类和老类字段数和字段名要一致;
- 新类和老类新增或删除的方法必须是 private static/final 修饰的;
- 可以修改方法体。
除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。