声明:本文首发于京东零售技术公众号,为博主本人撰写投稿。
JVM运行时Agent
在JDK1.6版本中,SUN更进一步,提供了可以在JVM运行时代理的能力,和启动时代理类似,只需要满足:
- JAR包的MANIFEST.MF清单文件中定义Agent-Class属性,指定一个类,加入
Can-Redefine-Classes
和Can-Retransform-Classes
选项。 - JAR包中包含清单文件中定义的这个类,类中包含
agentmain
方法,方法逻辑可以自己实现
运行时Agent可以在JVM运行时动态的修改某个类的字节码,然后JVM会重定义这个类**(不需要创建新的类加载器)**,但是为了保证JVM的正常运行,新定义的类相较于原来的类需要满足:
- 父类是同一个
- 实现的接口数也要相同,并且是相同的接口
- 类访问符必须一致
- 字段数和字段名要一致
- 新增或删除的方法必须是
private static/final
的 - 可以修改方法内部代码
运行时Agent需要借助JVM的Attach机制,简单来说就是JVM提供的一种通信机制,JVM中会存在一个Attach Listener
线程,监听其他JVM的attach请求,其通信方式基于socket,JVM Attach
机制大体流程图如下:
SUN在JDK中提供了Attach机制的Java语言工具包(com.sun.tools.attach)
,方便开发者使用Java语言进行操作,这里我们使用其中提供的loadAgent
方法实现运行中agent
的能力。
public class AttachUtil {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
// 获取运行中的JVM列表
List<VirtualMachineDescriptor> vmList = VirtualMachine.list();
// 需要agent的jar包路径
String agentJar = "xxxx/agent-test.jar";
for (VirtualMachineDescriptor vmd : vmList) {
// 找到测试的JVM
if (vmd.displayName().endsWith("WorkerMain")) {
// attach到目标ID的JVM上
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
// agent指定jar包到已经attach的JVM上
virtualMachine.loadAgent(agentJar);
virtualMachine.detach();
}
}
}
同时对之前启动时Agent的代码进行改写:
public class AgentMain {
// JVM启动时agent
public static void premain(String args, Instrumentation inst) {
agent0(args, inst);
}
// JVM运行时agent
public static void agentmain(String args, Instrumentation inst) {
agent0(args, inst);
}
public static void agent0(String args, Instrumentation inst) {
System.out.println("agent is running!");
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 打印transform的类名
System.out.println(className);
return classfileBuffer;
}
},true);
try {
// 找到WorkerMain类,对其进行重定义
Class<?> c = Class.forName("test.WorkerMain");
inst.retransformClasses(c);
} catch (Exception e) {
System.out.println("error!");
}
}
}
这里我们也没有对字节码进行修改,还是直接返回原本的字节码。运行AttachUtil
类,在目标JVM运行时完成了对其中test.WorkerMain
类的重新定义(虽然并没有修改字节码)。
下面从JDK源码层面对整个流程进行浅析:
当AttachUtil
的loadAgent
方法调用时,目标JVM会调用自身的Agent_OnAttach
方法,这个方法和之前提到的Agent_OnLoad
方法类似,会进行Agent JAR包的解析,不同的是Agent_OnAttach
方法会直接注册ClassFileLoadHook
事件回调函数,然后执行agentmain
方法添加类转换器。
需要注意的是我们在Java代码里调用了Instrumentation#retransformClasses(Class<?>...)
方法,追踪代码可以发现最终调用了一个native方法,而这个native方法的实现则在jdk的src\share\instrument\JPLISAgent.c
类中,最终retransformClasses
会调用到JVMTI的RetransformClasses
方法,这里由于JVM源码实现非常复杂,感兴趣的同学可以自行阅读(hotspot源码路径src\share\vm\prims\jvmtiEnv.cpp
),简单来说在这个方法里,JVM会触发ClassFileLoadHook
事件回调完成类字节码的转换,并完成虚拟机内已经加载的类字节码的热替换。
至此,在JVM运行时悄无声息的完成了类的重定义,不得不佩服JDK开发者的高超手段。