声明:本文首发于京东零售技术公众号,为博主本人撰写投稿。
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列表 ListvmList = 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的srcshareinstrumentJPLISAgent.c类中,最终retransformClasses会调用到JVMTI的RetransformClasses方法,这里由于JVM源码实现非常复杂,感兴趣的同学可以自行阅读(hotspot源码路径srcsharevmprimsjvmtiEnv.cpp),简单来说在这个方法里,JVM会触发ClassFileLoadHook事件回调完成类字节码的转换,并完成虚拟机内已经加载的类字节码的热替换。
至此,在JVM运行时悄无声息的完成了类的重定义,不得不佩服JDK开发者的高超手段。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)