java Agent

java Agent

java Agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包。只是启动方式和普通的jar包有所不同,对于普通的jar包,通过指定类的main函数进行启动。但是java Agent并不能单独启动,必须依附在一个java应用程序运行,在AOP方面应用比较广泛。

Java agent 的jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。主要功能如下:

  • 可以在加载class文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更,但是这种情况下会有很多的限制
  • 还有其他的一些小众的功能

    • 获取所有已经被加载过的类
    • 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
    • 获取某个对象的大小
    • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

javaagent是一种能够在不影响正常编译的情况下,修改字节码的技术。JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法。
javaagent是java命令的一个参数,参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain()方法。

类冲突问题

类冲突问题并非仅存在于JavaAgent场景中,在Java场景中一直都存在,该问题通常会导致运行时触发NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError等异常。

从使用场景来看,基于JavaAgent技术所实现的工具,往往用于监控、治理等场景,并非企业核心业务程序。如果在使用时引入类冲突问题,可能会造成核心业务程序故障,得不偿失,所以避免向核心业务程序引入类冲突是一个JavaAgent工具的基本要求。

还有一个重要原因是在Java应用中可以于开发态采用依赖的升降级、统一依赖架构治理等手段解决该问题。但基于JavaAgent技术实现的工具作用于运行态,无法在开发态就和需要被增强的Java应用进行统一的依赖管理,所以引入类冲突问题的可能性更大。

Instrumentation工具包

JDK 从5.0开始,提供了一个名为java.lang.instrument的工具包:

借助该包,开发者可以构建一个独立于应用程序的代理(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够动态替换和修改某些类的定义。这样的特性实际上提供了一种虚拟机级别的 AOP 实现。

事实上,java.lang.instrument 包是基于JVMTI机制实现的:

JVMTI(Java Virtual Machine Tool Interface)是一套由java虚拟机提供的一套代理程序机制,可以支持第三方程序一代理的方式连接和访问JVM。JVMTI功能非常丰富,包括虚拟机中线程、内存/堆/栈,类/方法/变量,事件/定时器处理等等。使用JVMTI一个基本的方式就是设置回调函数,在某些事件发生的时候触发并做出相应的动作,这些事件包含虚拟机初始化,开始运行、结束,类的加载,方法出入,线程始末等等。

Instrument 就是一个基于 JVMTI 接口的,以代理方式连接和访问 JVM 的一个 Agent。

实例

下面给出一个简单的Java应用程序,介绍JavaAgent的应用实例:

public class HelloAgent {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("Hello Agent!");
    }
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

在该Java应用程序中,我们定义了一个类HelloAgent,并且在其中实现了premain()方法。在premain()方法中,我们简单地输出了一句话"Hello Agent!"。此外,我们还定义了一个main()方法,在其中输出一句话"Hello World!"。

现在我们需要在Java应用程序启动时,自动执行HelloAgent的premain()方法,实现对Java应用程序的增强。为此,我们需要使用Java命令行工具来启动Java应用程序,并且指定JavaAgent参数,如下所示:

java -javaagent:HelloAgent.jar -jar MyApp.jar

在上面的命们通过-javaagent参数来指定HelloAgent.jar文件,利用JavaAgent的特性,向MyApp.jar应用程序中动态地添加新的功能。

基本原理

javaAgent利用java提供的Instrumentation Api来对java应用程序进行增强。在java启动时,如果传递了-javaagent参数,则jvm会在启动应用程序之前初始化javaagent,并且调用它的premain()方法来执行java应用程序的代理过程。如果在应用程序启动后需要增强应用程序功能,则可以通过attcach的方式在java应用程序运行时获取Instrumentation 对象,并运行javaAgent的agentmain()方法来实现java应用程序的增强。

JavaAgent主要涉及class redefinition、class retransformation和instrumentation等Instrumentation API,其中,class redefinition可以实现对已经加载到JVM中的class的重新定义,而class retransformation可以实现对已经加载到JVM中的class的重新转化。Instrumentation API则提供了对Java应用程序字节码的种种操作。通过这些API,JavaAgent就可以实现Java应用程序的增强。

java agent的实现原理涉及到java的Instrumentation Api和java虚拟机类加载机制。java的Instrumentation Api可以再类加载即转换阶段对类进行插桩,而类加载机制可以为javaAgent提供必要的环境,以便实现对java应用程序的增强。

Java的Instrumentation API提供了premain()和agentmain()两个入口。其中,premain()方法在Java应用程序启动前调用,而agentmain()方法可以在应用程序运行时动态加载JavaAgent,并开始执行增强操作。

Java的类加载机制是javaAgent实现的关键。java虚拟机在类装在阶段采用双亲委派模型,即先委派父类加载器加载类,如果父类加载器无法加载类,则交给子类加载器加载。javaAgent可以通过对类加载器的替换和重载,实现对Java应用程序的类进行增强,从而实现Java应用程序的性能监测、AOP编程、业务逻辑封装、安全性增强等功能。

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

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

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

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

运行时修改

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

  • ClassFileLoadHook
  • TranFormClassFile

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

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

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

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


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

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

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

其中整体的执行依赖于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。

原文java agent技术原理及简单实现_javaagent-CSDN博客

Last modification:November 27, 2023
如果觉得我的文章对你有用,请随意赞赏