Java Agent介绍

Java Agent注入的使用方法

在进程B中向进程A中注入java agent,需要满足以下条件:

  1. java agent中的所有依赖,在进程A中的classpath中都要能找到,否则在注入时进程A会报错NoClassDefFoundError
  2. java agent的pom文件中包含如下内容,以在jar包中包含MANIFEST.MF并设置Agent-Class和Can-Retransform-Classes属性
  3. 进程B的classpath中必须有tools.jar(提供VirtualMachine attach api),jdk默认有tools.jar,jre默认没有。
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.pluginsgroupId>
      <artifactId>maven-jar-pluginartifactId>
      <version>2.6version>
      <configuration>
        <archive>
          <manifestEntries>
            <Agent-Class>com.warrenyoung.instrumentions.agent.AgentMainAgent-Class>
            <Can-Retransform-Classes>trueCan-Retransform-Classes>
          manifestEntries>
        archive>
      configuration>
    plugin>
  plugins>
build>

java agent开发和生效流程

  1. 开发带有agentmain/premain方法的class,添加ClassFileTransformer的子类,显式调用retransformClasses函数,并在MANIFEST.MF中设置上文中的属性
  2. 实现ClassFileTransformer类的transform方法,进行代码注入,可借助于javassist工具
  3. 在另一个java程序app2中,查找到需要注入代码的JVM,attach java agent的jar包到指定的JVM中
  4. 示例工程位置:instrumentiontest
// AgentMain.class
public class AgentMain {
    public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
        inst.addTransformer(new TransformerTest(), true);
        inst.retransformClasses(InstrumentTestClass.class);
        System.out.println("Agent Main Done");
    }
}

// TransformerTest.class
public class TransformerTest implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (!className.equals("com/warrenyoung/instrumentions/instrumentiondest/InstrumentTestClass"))
        {
            System.out.println(className);
            return classfileBuffer;
        }
        try {
            System.out.println("x:" + className);
            ClassPool classPool = new ClassPool();
            classPool.appendClassPath(new LoaderClassPath(loader));
            final CtClass ctClass;
            try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(classfileBuffer)) {
                ctClass = classPool.makeClass(byteArrayInputStream);
            }
            ctClass.getDeclaredMethod("getStr").setBody("return \"b\";");
            ctClass.getDeclaredMethod("getNum").setBody("return 2;");
            System.out.println("transformed");
            return ctClass.toBytecode();
        } catch (Exception e)
        {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

// 用于执行注入动作的app2
public class Main {

    private static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    public static void main(String[] args)
    {
        // 这里其实不需要循环,单次注入,被注入JVM终生受影响,除非其他行为又触发了class被重新加载
        executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                run1();
            }
        }, 2000, 20000000, TimeUnit.MILLISECONDS);
    }

    public static void run1() {
        List<VirtualMachineDescriptor> listAfter = null;
        try {
            int count = 0;
                listAfter = VirtualMachine.list();
                for (VirtualMachineDescriptor vmd : listAfter) {
                    if (vmd.displayName().equals("com.warrenyoung.instrumentions.instrumentiondest.Main"))
                    {
                        VirtualMachine vm = VirtualMachine.attach(vmd);
                        vm.loadAgent("/Users/warrenyoung/develops/instrumentiontest/agenttest/target/agenttest-1.0-SNAPSHOT.jar");
                        vm.detach();
                    }
                    System.out.println(vmd.displayName() + ", " + vmd.id());
                }
        } catch (Exception e) {

        }
    }
}

Java Agent的使用场景

IDE启动的时候会使用 -javaagent参数,如

/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/bin/java “-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55984:/Applications/IntelliJ IDEA.app/Contents/bin” …com.warrenyoung.instrumentions.instrumentiondest.Main

Java agent工作原理

JVMTI是JVM Tool Interface,是JVM开放的一些供开发者扩展的native编程接口,基于事件驱动,开发者一般设置一些callback接口,对应事件(如:虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等)被触发时,callback会被调用。
JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。
-agentlib:agent-lib-name=options
-agentpath:path-to-agent=options
Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:

  • 所有的 Java 类都未被初始化;
  • 所有的对象实例都未被创建;
  • 因而,没有任何 Java 代码被执行;

JVMTI Agent可以实现三个方法

  • JVM启动的时候如果设置了Java Agent,会执行OnLoad
    JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
  • JVM运行时使用attach的方式动态加载Java Agent,会执行OnAttach
    JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);
  • JVM关闭或者动态加载的Java Agent detach的时候会执行OnLoad
    JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)

JVMTI的一种典型应用是Java调试功能,这主要包括了设置断点、调试(step)等,在 JVMTI 里面,设置断点的 API 本身很简单:
jvmtiError SetBreakpoint(jvmtiEnv* env, jmethodID method, jlocation location)
我们常用的Java Agent是基于一个叫做Instrument的JVMTI Agent实现的,Instrument Agent实现了OnLoad, OnAttach方法。
Instrumentation的redefineClasses和retransformClasses功能相似,redefineClasses是Java5引入的,retransformClasses是Java6引入的。
在Instrumentation.addTransformer,添加ClassFileTransformer,在以下三种情形下ClassFileTransformer.transform会被执行

  • 新的class被加载
  • Instrumentation.redefineClasses显式调用
  • addTransformer第二个参数为true时,Instrumentation.retransformClasses显式调用

所以java agent的两种工作模式下,

  • 如果是以 -javaagent方式使用的,则不需要在premain中显式调用retransformClasses/redefineClasses,因为premain方法是在所有用户类被加载之前执行的
  • 如果是以attach的方式使用的,则可能需要在agentmain中显式调用retransformClasses/redefineClasses,因为attach的时候用户类可能已经被加载过了

在第一种方式下,由于java agent premain方法是在所有用户类被加载之前执行的,transform前后的类的结构可以完全不同;第二种方式下,由于java agent agentmain方法执行的时候,部分类已经被加载过了,如果需要重新加载已加载的类,为了保证transform之后的类仍然可用,要求新的类格式与老的类格式兼容,因为transform只是更新了类里内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新带来的开销。限制如下:

  • 父类是同一个
  • 实现的接口数也要相同,并且是相同的接口
  • 类访问符必须一致
  • 字段数和字段名要一致
  • 新增或删除的方法必须是private static/final的
  • 可以修改方法

Class Redefine的实现

类重新定义,这是Instrumentation提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,通过InstrumentationImpl的下面的redefineClasses方法去操作了:

public void redefineClasses(ClassDefinition[] definitions) throws ClassNotFoundException {
        if (!isRedefineClassesSupported()) {
            throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
        }
        if (definitions == null) {
            throw new NullPointerException("null passed as 'definitions' in redefineClasses");
        }
        for (int i = 0; i < definitions.length; ++i) {
            if (definitions[i] == null) {
                throw new NullPointerException("element of 'definitions' is null in redefineClasses");
            }
        }
        if (definitions.length == 0) {
            return; // short-circuit if there are no changes requested
        }

        redefineClasses0(mNativeAgent, definitions);
    }

在JVM里对应的实现是创建一个VM_RedefineClassesVM_Operation,注意执行它的时候会stop the world的:

jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
  VMThread::execute(&op);
  return (op.check_error());
} /* end RedefineClasses */

这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:

  • 挨个遍历要批量重定义的jvmtiClassDefinition
  • 然后读取新的字节码,如果有关注ClassFileLoadHook事件的,还会走对应的transform来对新的字节码再做修改
  • 字节码解析好,创建一个klassOop对象
  • 对比新老类,要求前后格式兼容(具体格式要求见上文)
  • 对新类做字节码校验
  • 合并新老类的常量池
  • 如果老类上有断点,那都清除掉
  • 对老类做jit去优化
  • 对新老方法匹配的方法的jmethodid做更新,将老的jmethodId更新到新的method上
  • 新类的常量池的holer指向老的类
  • 将新类和老类的一些属性做交换,比如常量池,methods,内部类
  • 初始化新的vtable和itable
  • 交换annotation的method,field,paramenter
  • 遍历所有当前类的子类,修改他们的vtable及itable

上面是基本的过程,总的来说就是只更新了类里内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新带来的开销。

Class Retransform的实现

Java 5就提供了Class Redifine的能力,而Java 6才支持Class Restransform,可以认为Restransform是Redifine的一种升级版本,更加方便使用,两者能实现的功能是一致的,只是调用方式有些区别。
Instrumentation.retransform过程中会对每个class逐个调用使用Instrumentation.addTransformer添加的ClassFileTransformer的transform方法,多个ClassFileTransformer之间的变更具备传递性。

    public void retransformClasses(Class[] classes) {
        if (!isRetransformClassesSupported()) {
            throw new UnsupportedOperationException(
              "retransformClasses is not supported in this environment");
        }
        retransformClasses0(mNativeAgent, classes);
    }

你可能感兴趣的:(java)