javaAgent概念说明

157 阅读13分钟

Java Agent 技术简介

Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。

Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。

image.png

Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作。启动时只需要在目标程序的启动参数中添加-javaagent 参数添加 ClassFileTransformer 字节码转换器,相当于在main方法前加了一个拦截器。

Java Agent 功能介绍

Java Agent 主要有以下功能

  • Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
  • Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;

Java Agent 的应用场景

  • IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
  • 热部署功能,例如 JRebel、XRebel、spring-loaded;
  • 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
  • 各种性能分析工具,例如 Visual VM、JConsole 等;
  • 全链路性能检测工具,例如 Skywalking、Pinpoint等;

关键术语介绍

image.png

  • 「 JVMTI 」 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展

    JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现

    形象地说,JVMTI是Java虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控,分析。甚至干预虚拟机的运行。

  • 「 JVMTIAgent 」 是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

    • Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
    • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
    • Agent_OnUnload函数,在agent卸载时调用
  • 「 instrument」 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent

  • 「 javaagent 」 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的

  • 「JVM Attach」 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行

Instrument介绍

核心方法

方法名作用
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用
void addTransformer(ClassFileTransformer transformer)也就是上面方法canRetransform为false时, 表示通过ClassFileTransformer实例重定义的类不能进行回滚
boolean removeTransformer(ClassFileTransformer transformer)移除ClassFileTransformer实例
boolean isRetransformClassesSupported()返回当前JVM配置是否支持类重新转换
void retransformClasses(Class<?>... classes)重新转换类, 根据已经存在的字节码文件, 就行修改后再替换
boolean isRedefineClassesSupported()返回当前JVM配置是否支持重定义类(修改类的字节码)
void redefineClasses(ClassDefinition... definitions)重新定义类, 以自己提供的字节码文件替换已存在的class文件

redefineClasses(重定义)

void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

类加载过程中, 类加载器的defineClass()方法会将二进制文件读取得到class对象, redefineClasses就是可控的再次进行这一过程. 对于已经加载的类, 该方法通过使用提供的新的字节码文件进行替换, 现有的类文件字节不会被使用,就像从源头进行重新编译以进行修复和继续调试时一样。(只是修改类内存中的字节码,并不会重新做初始化动作) 参考:www.cnblogs.com/duanxz/p/49…

image.png

重定义的类的特点
  1. 该方法对一组class进行操作,以便同时允许多个相互依赖的类的更改,如A类的重新定义可能需要重新定义B类。
  2. 重新定义一个类并不会导致它的初始化器被运行。 静态变量的值将保持在调用之前。重新定义的类的实例不受影响。
  3. 重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系。
  4. 类文件字节不会被检查,验证和安装,直到应用转换为止,如果结果字节错误,则此方法将抛出异常。如果此方法抛出异常,则不会重新定义任何类。
  5. 如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。
  6. 如果此方法抛出异常,则不会重定义任何类。

1、redefineClasses 方法通过修改 JVM 内部的 Class 对象数据来实现类的重新定义。当你使用 redefineClasses 方法时,实际上是在更新 JVM 中已经加载的类的字节码表示。这种操作不涉及重新加载类,而是直接修改现有的 Class 对象的数据结构。
2、 Class 对象不变:虽然 Class 对象的数据(如方法字节码)被更新,但 Class 对象本身在 JVM 中的标识(如类加载器和类的唯一性)保持不变。这意味着类的静态字段、类加载器、包信息等不会因为重新定义而改变。

示例代码

以下是一个简单的示例,展示如何使用 redefineClasses 方法:

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        try {
            // 假设我们已经生成了新的字节码
            byte[] newBytecode = ...;

            // 创建 ClassDefinition
            ClassDefinition def = new ClassDefinition(MyClass.class, newBytecode);

            // 重新定义类
            inst.redefineClasses(def);
        } catch (ClassNotFoundException | UnmodifiableClassException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,MyClass 是需要重新定义的类,而 newBytecode 是它的新字节码。这个过程通常需要在 JVM 启动时通过代理实现。

retransformClasses(重定义)

void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

retransformClasses 是 Java Instrumentation API 中的一部分,用于在运行时重新转换已经加载的类。与 redefineClasses 不同,retransformClasses 允许对类进行重新转换,而不需要提供新的字节码。相反,它使用预先注册的 ClassFileTransformer 来修改类的字节码。以下是对 retransformClasses 的详细介绍:

  1. 重新转换类

    • retransformClasses 使得已经加载的类可以被重新转换。通过调用此方法,JVM 会重新调用每个注册的 ClassFileTransformer,并将原始字节码提供给它们进行转换。
  2. 不需要提供新的字节码

    • 与 redefineClasses 需要提供新的字节码不同,retransformClasses 使用现有的 ClassFileTransformer 逻辑来对类进行转换。
  3. 支持多次转换

    • 一个类可以被多次重新转换,每次转换时都会调用所有注册的 ClassFileTransformer
代码示例
1. 创建目标类

首先,创建一个简单的目标类,称为 HelloWorld

public class HelloWorld {
    public void sayHello() {
        System.out.println("Hello, World!");
    }

    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.sayHello();
    }
}
2. 实现 ClassFileTransformer

接下来,实现一个 ClassFileTransformer,用于修改 HelloWorld 类的方法:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class SimpleTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
        ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) throws IllegalClassFormatException {

        if ("HelloWorld".equals(className.replace("/", "."))) {
            System.out.println("Transforming HelloWorld class");

            // 这里可以使用 ASM 或 Javassist 来修改字节码
            // 为了简单起见,假设我们只是打印一条消息
            // 实际应用中需要返回修改后的字节码

            // 返回未修改的字节码
            return classfileBuffer;
        }
        return null;
    }
}
3. 创建 Java Agent

创建一个 Java Agent 来注册 ClassFileTransformer 并调用 retransformClasses

import java.lang.instrument.Instrumentation;

public class TransformerAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        SimpleTransformer transformer = new SimpleTransformer();
        inst.addTransformer(transformer, true);

        try {
            // Retransform the HelloWorld class
            Class<?> targetClass = Class.forName("HelloWorld");
            inst.retransformClasses(targetClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
4. 创建 MANIFEST.MF

创建一个 MANIFEST.MF 文件,用于指定代理类:

Premain-Class: TransformerAgent

5. 编译并打包

编译所有类,并将它们打包到一个 JAR 中:

javac HelloWorld.java SimpleTransformer.java TransformerAgent.java
jar cmf MANIFEST.MF agent.jar HelloWorld.class SimpleTransformer.class TransformerAgent.class
6. 运行程序

使用 Java Agent 运行程序:

java -javaagent:agent.jar -cp . HelloWorld

Transformer 简介

ClassFileTransformer 是 Java Instrumentation API 的一个接口,用于在类被加载到 JVM 时动态修改其字节码。通过实现这个接口,我们可以在类加载的过程中插入、修改或删除字节码,以实现各种功能,如性能监控、日志记录、代码增强等。

ClassFileTransformer 接口

ClassFileTransformer 接口只有一个方法需要实现:

byte[] transform(
    ClassLoader loader,
    String className,
    Class<?> classBeingRedefined,
    ProtectionDomain protectionDomain,
    byte[] classfileBuffer) throws IllegalClassFormatException;

方法参数详解

  • ClassLoader loader:

    • 加载当前类的类加载器。如果类是由引导类加载器加载的,则为 null
  • String className:

    • 类的名称,使用内部格式表示(例如,java/lang/String)。
  • Class<?> classBeingRedefined:

    • 如果这是一个重定义或重新转换的类,这是被重定义或重新转换的类对象;否则为 null
  • ProtectionDomain protectionDomain:

    • 当前类的保护域。
  • byte[] classfileBuffer:

    • 类的字节码,格式为 Class 文件格式。

返回值

  • 返回修改后的字节码。如果返回 null,表示不对类进行修改。
  • 如果修改后的字节码无效,可能会导致 ClassFormatError 或其他错误。

异常

  • IllegalClassFormatException:

    • 如果字节码格式不正确,可以抛出这个异常。

触发时机

ClassFileTransformer 的触发时机主要与类的加载和重转换过程有关。以下是具体的触发时机:

1. 类首次加载时

  • 当 JVM 首次加载某个类时,如果已经注册了 ClassFileTransformer,则会触发 transform 方法。
  • 这是最常见的触发时机,适用于在类加载时进行字节码修改的场景。

2. 类重转换时

  • 如果使用 InstrumentationretransformClasses 方法,ClassFileTransformer 也会被触发。
  • retransformClasses 允许你在类已经加载后再次修改其字节码。这种情况下,transform 方法会在重转换时被调用。
  • 注意:在重转换过程中,不能改变类的结构(如添加或删除方法、字段),只能修改方法的实现。

3. 类重定义时

  • 使用 InstrumentationredefineClasses 方法可以重定义类,这会触发 transform 方法。
  • 与重转换不同,重定义可以改变类的结构,但需要提供完整的修改后的字节码。

触发机制的注意事项

  • 类加载顺序ClassFileTransformer 只会在类首次加载或重转换时触发,因此在类已经加载后注册的 Transformer 不会对已加载的类生效,除非通过 retransformClasses 显式重转换。

  • 多次触发:如果多个 ClassFileTransformer 被注册,那么每个 transformer 都会在类加载或重转换时依次触发。后续的 transformer 接收到的是前一个 transformer 修改后的字节码。

  • 性能影响:频繁的字节码转换可能会影响性能,尤其是在大规模应用中,因此使用时需要谨慎。

通过了解这些触发时机,你可以更有效地使用 ClassFileTransformer 来实现动态字节码修改。

使用步骤

  1. 实现 ClassFileTransformer

    • 创建一个类实现 ClassFileTransformer 接口,并实现 transform 方法。
  2. 注册 Transformer

    • 使用 Instrumentation 实例的 addTransformer 方法注册你的 ClassFileTransformer
  3. 修改字节码

    • 在 transform 方法中,使用字节码操作库(如 ASM、Javassist)来修改字节码。

触发transform时机

示例

以下是一个简单的 ClassFileTransformer 示例,展示如何在类被加载时打印类名:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class SimpleTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
        ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println("Loading class: " + className);

        // 这里没有进行实际的字节码修改,直接返回原始字节码
        return classfileBuffer;
    }
}

retransformClasses 和 redefineClasses 区别

Instrumentation 接口提供了两种主要方法来动态修改已经加载的类:retransformClassesredefineClasses。这两者的区别主要在于它们对类结构的影响和使用场景:

retransformClasses

  • 功能:允许对已经加载的类进行重转换。
  • 限制:不能改变类的结构。具体来说,不能添加或删除类的字段、方法,不能改变方法的签名或类的继承结构。
  • 使用场景:适用于需要修改方法实现或插入代码逻辑的场景,而不需要改变类的结构。
  • 触发机制:当调用 retransformClasses 时,JVM 会再次调用注册的 ClassFileTransformertransform 方法,以便对类进行重转换。

redefineClasses

  • 功能:允许对已经加载的类进行重定义。
  • 灵活性:可以改变类的结构,包括添加或删除字段、方法,改变方法的签名等。
  • 使用场景:适用于需要对类结构进行更改的场景,如添加新的方法或字段。
  • 实现复杂度:使用 redefineClasses 需要提供完整的类字节码(包括所有方法、字段),因为它会完全替换现有的类定义。
  • 触发机制redefineClasses 不依赖于 ClassFileTransformer,而是直接通过提供完整的字节码来实现类的重定义。

选择使用哪种方法

  • 如果你的需求仅仅是修改方法的实现(例如插入日志、性能监控代码),而不改变类的结构,retransformClasses 是更合适的选择。
  • 如果需要更改类的结构(例如添加新的方法或字段),则必须使用 redefineClasses

在使用这两种方法时,需要注意性能影响和可能的兼容性问题,特别是在涉及到复杂的类结构或在生产环境中使用时。确保在测试环境中充分验证修改后的类行为,以避免引入潜在的错误或不兼容性。

Attach

Instrument能够在jvm运行时加载, 就是通过Attach的能力, 因此这里简单介绍一下.

Attach是什么?

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM “附着”(Attach)代理工具程序的。

Attach的功能

简单来说attach是一种进程间通信的工具. 可以用于jvm进程间通信 ,能让一个进程传命令给另一个进程, 命令另一个进程进行一些操作.

image.png

Attach机制可以对目标进程收集很多信息,如内存dump,线程dump,类信息统计(比如加载的类及大小以及实例个数等),动态加载agent,获取系统属性等等。

典型的应用例如jstack工具:

当我们需要某一个正在运行的jvm进程的线程使用情况时, 会运行jstack进程, 然后告诉它目标进程的pid, jstack就会利用attach机制在进程间进行通信, 完成相应数据的获取.

参考、

这个很好 很全面:www.cnblogs.com/crazymakerc…

链接基本概念及demo:juejin.cn/post/708602…

链接:juejin.cn/post/691900…

链接javaAgent原理及demo说明:www.cnblogs.com/rickiyang/p…

链接:juejin.cn/post/716923…

SPI 热部署:juejin.cn/post/725806…

juejin.cn/post/715768…

参考:
JVM插庄之二:Java agent基础原理
Java 动态调试技术原理及实践 # Java—JavaAgent探针 Agent 内存马的攻防之道 字节码增强宝藏