用户您好!请先登录!

Java Agent那点事

Java Agent那点事

1. Java agent介绍

1.1 什么是Java agent?

Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。

Java agent技术的主要功能如下:

  • 可以在加载java文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更
  • 还有其他的一些小众的功能:
    • 获取所有已经被加载过的类
    • 获取所有已经被初始化过了的类
    • 获取某个对象的大小
    • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

1.2 Java Instrumentation API

通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。

1.2.1 实现agent启动方法

Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:

[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

[1] public static void agentmain(String agentArgs, Instrumentation inst); 
[2] public static void agentmain(String agentArgs);

这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

1.2.2 指定Main-Class

Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”,且需根据需求定义Can-Redefine-Classes和Can-Retransform-Classes:

Manifest-Version: 1.0
preMain-Class:com.test.Agent
ClassArchiver-Version:Plexus 
ArchiverAgent-Class: com.test.Agent
ClassCan-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_112
1.2.3 Agent加载
  • 启动时加载启动参数增加-javaagent:[path],其中path为对应的agent的jar包路径
  • 运行中加载使用com.sun.tools.attach.VirtualMachine加载
try {  
    String jvmPid = 目标进行的pid;  
    logger.info("Attaching to target JVM with PID: " + jvmPid);  
    VirtualMachine jvm = VirtualMachine.attach(jvmPid);  
    jvm.loadAgent(agentFilePath);//agentFilePath为agent的路径  
    jvm.detach();  
    logger.info("Attached to target JVM and loaded Java agent successfully");
} catch (Exception e) {  
    throw new RuntimeException(e);
}
1.2.4 Instrument

Instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

首先可以定义如下的类转换器:

public class TestTransformer implements ClassFileTransformer {
  //目标类名称,  .分隔      
  private String targetClassName;    
  //目标类名称,  /分隔    
  private String targetVMClassName;
  private String targetMethodName;

  public TestTransformer(String className,String methodName){
    this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
    this.targetMethodName = methodName;
    this.targetClassName=className;
  }    

  //类加载时会执行该函数,其中参数 classfileBuffer为类原始字节码,返回值为目标字节码,className为/分隔    
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    //判断类名是否为目标类名          
    if(!className.equals(targetVMClassName)){            
      return classfileBuffer;        
    }        
    try {            
      ClassPool classPool = ClassPool.getDefault();            
      CtClass cls = classPool.get(this.targetClassName);            
      CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
      ctMethod.insertBefore("{ System.out.println(\"start\"); }"); 
      ctMethod.insertAfter("{ System.out.println(\"end\"); }");            
      return cls.toBytecode();        
    } catch (Exception e) {
      // do something;
    }        
    return classfileBuffer;    
  }
}

类转换器定义完毕后,需要将定义好的类转换器添加到对应的instrmentation中,对于已经加载过的类使用retransformClasses对类进行重新加载:

public class AgentDemo {    
  private static String className = "hello.GreetingController";    
  private static String methodName = "getDomain";    

  public static void agentmain(String args, Instrumentation instrumentation) {
    try {            
      List<Class> needRetransFormClasses = new LinkedList<>();
      Class[] loadedClass = instrumentation.getAllLoadedClasses();
      for (int i = 0; i < loadedClass.length; i++) {
        if (loadedClass[i].getName().equals(className)) {
          needRetransFormClasses.add(loadedClass[i]);                
        }            
      }            
      instrumentation.addTransformer(new TestTransformer(className, methodName));
      instrumentation.retransformClasses(needRetransFormClasses.toArray(new Class[0]));
    } catch (Exception e) {
      // do something;
    }    
  }    

  public static void premain(String args, Instrumentation instrumentation) {
    instrumentation.addTransformer(new TestTransformer(className, methodName));
  }
}

从上图的代码可以看出,主方法实现了两个,分别为agentmain和premain,其中:

  • premain用于在启动时,类加载前定义类的TransFormer,在类加载的时候更新对应的类的字节码
  • agentmain用于在运行时进行类的字节码的修改,步骤整体分为两步注册类的TransFormer调用retransformClasses函数进行类的重加载

 

2. Java agent原理简述

2.1 启动时修改

工作五年程序员一枚,不了解Java agent技术,感觉要凉

启动时修改主要是在jvm启动时,执行native函数的Agent_OnLoad方法,在方法执行时,执行如下步骤:

  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法

 

2.2 运行时修改

工作五年程序员一枚,不了解Java agent技术,感觉要凉

运行时修改主要是通过jvm的attach机制来请求目标jvm加载对应的agent,执行native函数的Agent_OnAttach方法,在方法执行时,执行如下步骤:

  • 创建InstrumentationImpl对象
  • 监听ClassFileLoadHook事件
  • 调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里MANIFEST.MF里指定的Agentmain-Class类的agentmain方法

 

2.3 ClassFileLoadHook和TransFormClassFile

在2.1和2.2节中,可以看出整体流程中有两个部分是具有共性的,分别为:

  • ClassFileLoadHook
  • TranFormClassFile

ClassFileLoadHook是一个jvmti事件,该事件是instrument agent的一个核心事件,主要是在读取字节码文件回调时调用,内部调用了TransFormClassFile函数。

TransFormClassFile的主要作用是调用java.lang.instrument.ClassFileTransformer的tranform方法,该方法由开发者实现,通过instrument的addTransformer方法进行注册。

通过以上描述可以看出在字节码文件加载的时候,会触发ClassFileLoadHook事件,该事件调用TransFormClassFile,通过经由instrument的addTransformer注册的方法完成整体的字节码修改。

对于已加载的类,需要调用retransformClass函数,然后经由redefineClasses函数,在读取已加载的字节码文件后,若该字节码文件对应的类关注了ClassFileLoadHook事件,则调用ClassFileLoadHook事件。后续流程与类加载时字节码替换一致。

2.4 何时进行运行时替换?

在类加载完毕后,对应的想要替换函数可能正在执行,那么何时进行类字节码的替换呢?

由于运行时类字节码替换依赖于redefineClasses,那么可以看一下该方法的定义:

jvmtiErrorJvmtiEnv::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 */

其中整体的执行依赖于VMThread,VMThread是一个在虚拟机创建时生成的单例原生线程,这个线程能派生出其他线程。同时,这个线程的主要的作用是维护一个vm操作队列(VMOperationQueue),用于处理其他线程提交的vm operation,比如执行GC等。

VmThread在执行一个vm操作时,先判断这个操作是否需要在safepoint下执行。若需要safepoint下执行且当前系统不在safepoint下,则调用SafepointSynchronize的方法驱使所有线程进入safepoint中,再执行vm操作。执行完后再唤醒所有线程。若此操作不需要在safepoint下,或者当前系统已经在safepoint下,则可以直接执行该操作了。所以,在safepoint的vm操作下,只有vm线程可以执行具体的逻辑,其他线程都要进入safepoint下并被挂起,直到完成此次操作。

因此,在执行字节码替换的时候需要在safepoint下执行,因此整体会触发stop-the-world。

行走的code

要发表评论,您必须先登录