入侵JVM?Java Agent原理浅析和实践(中)

声明:本文首发于京东零售技术公众号,为博主本人撰写投稿。

JVM运行时Agent

在JDK1.6版本中,SUN更进一步,提供了可以在JVM运行时代理的能力,和启动时代理类似,只需要满足:

  • JAR包的MANIFEST.MF清单文件中定义Agent-Class属性,指定一个类,加入Can-Redefine-ClassesCan-Retransform-Classes 选项。
  • JAR包中包含清单文件中定义的这个类,类中包含agentmain方法,方法逻辑可以自己实现

运行时Agent可以在JVM运行时动态的修改某个类的字节码,然后JVM会重定义这个类**(不需要创建新的类加载器)**,但是为了保证JVM的正常运行,新定义的类相较于原来的类需要满足:

  1. 父类是同一个
  2. 实现的接口数也要相同,并且是相同的接口
  3. 类访问符必须一致
  4. 字段数和字段名要一致
  5. 新增或删除的方法必须是private static/final
  6. 可以修改方法内部代码

运行时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源码层面对整个流程进行浅析:

AttachUtilloadAgent方法调用时,目标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开发者的高超手段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值