javaagent入门

介绍

java agent是一种java代理技术,JDK1.5引入,支持动态修改Java字节码。

功能和应用场景

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

原理

JVMTI

JVMTI (JVM Tool Interface)是 Java 虚拟机对外提供的 Native 编程接口,通过 JVMTI ,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
JVMTI 是一套 Native 接口,在 Java SE 5 之前,要实现一个 Agent 只能通过编写 Native 代码来实现。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。JVMTI和Instumentation API的作用很相似,都是一套 JVM 操作和监控的接口,且都需要通过agent来启动:

  • Instumentation API需要打包成 jar,并通过 Java agent 加载(对应启动参数: -javaagent)
  • JVMTI 需要打包成动态链接库(随操作系统,如.dll/.so文件),并通过 JVMTI agent 加载(对应启动参数: -agentlib/-agentpath)

JVMTIAgent

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

  • Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
  • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
  • Agent_OnUnload函数,在agent卸载时调用

JVM Attach

是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行,原理这篇文章讲的很详细:https://juejin.cn/post/7157684112122183693#heading-27

Instrumentation

实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent。
Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。

主要方法

addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。

public interface Instrumentation {
    /**
     * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
     * Transformer可以直接对类的字节码byte[]进行修改
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
     * retransformClasses可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    /**
     * 获取一个对象的大小
     */
    long getObjectSize(Object objectToSize);
    
    /**
     * 将一个jar加入到bootstrap classloader的 classpath里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);
    
    /**
     * 获取当前被JVM加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}
  • 最常用的方法是addTransformer(ClassFileTransformer transformer), 其参数ClassFileTransformer接口定义:
public interface ClassFileTransformer {
   
    /**
     * 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
     * 返回值为需要被修改后的字节码byte[]
     */
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

局限性

通过Instrumentation的redefineClasses方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:

不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过ASM获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。

java agent

主流的JVM都对Instrumentation进行了实现,但不直接提供在JDK的runtime里,而必须通过java agent。
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。

premain和agentmain

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。

  • JVM启动时加载的是premain(静态加载)
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
  • JVM运行时加载的是agentmain(动态加载)
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);

告示文件MANIFEST.MF

在resources下创建META-INF/MANIFEST.MF文件, 或者在Maven中配置编译打包的插件
告示文件MANIFEST.MF参数说明:

Manifest-Version
文件版本
Premain-Class
包含 premain 方法的类(类的全路径名)main方法运行前代理
Agent-Class
包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构
Boot-Class-Path
设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选
Can-Redefine-Classes true
表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes true
表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix true
表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
...
  • maven plugin配置示例
<!-- 设置manifest配置文件-->
<manifestEntries>
    <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
    <Premain-Class>demo.MethodAgentMain</Premain-Class>
    <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
    <Agent-Class>demo.MethodAgentMain</Agent-Class>
    <!--Can-Redefine-Classes: 是否可进行类定义。-->
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <!--Can-Retransform-Classes: 是否可进行类转换。-->
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
  • 生成的MANIFEST.MF,示例:
Premain-Class: demo.MethodAgentMain
Built-By: xxx
Agent-Class: demo.MethodAgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

静态启动参数

-javaagent:<jarpath>[=<option>] option指定 agent 的参数,可以传递到premain(String agentArgs, Instrumentation inst)方法的agentArgs入参中。支持可以定义多个agent,按指定顺序先后执行。

示例: java -javaagent:agent1.jar=key1=value1&key2=value2 -javaagent:agent2.jar -jar Test.jar
其中加载顺序为(1) agent1.jar (2) agent2.jar。
注意:不同的顺序可能会导致 agent 对类的修改存在冲突,在实际项目中用到了pinpoint和SkyWalking的agent,当通过-javaagent先挂载 pinpoint的 agent ,后挂载 SkyWalking的 agent,出现 SkyWalking对类的增强发生异常的情况,而先挂载SkyWalking的 agent 则无问题。
agent1.jar 的premain(String agentArgs, Instrumentation inst)方法的agentArgs值为key1=value1&key2=value2。

Ref

【JVM】Java agent超详细知识梳理
Java Agent 探针技术
javaagent使用指南
javaagent实战
破解 Java Agent 探针黑科技!
字节码增强技术探索
JVM源码分析之javaagent原理完全解读
javaagent-example-demo

posted @   惜阳茕影  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示