Java agent超详细知识梳理

网站建设4年前发布
34 00

梳理SkyWalking agentplugin、elasticsearch的plugin、arthas​等技术的原理时,发现他们的底层原理很多是相同的。这类工具都用到了Java agent、类加载、类隔离等技术,在此进行归类梳理。,本篇将梳理Java agent相关内容。在此先把这些技术整体的关系梳理如下:,Java agent超详细知识梳理,Java agent​是基于JVMTI(JVM Tool Interface​)实现,后面的内容将对Java agent和JVMTI做详细介绍。,Java agent​ 技术结合 Java Intrumentation API 可以实现类修改、热加载等功能。,Java agent和JVMTI 技术的常见应用场景:,Java agent超详细知识梳理,我们先用一个 Java agent​ 实现方法开始和结束时打印日志的简单例子来实践一下,通过示例,可以很快对后面 Java agent 技术有初步的理解。,创建 demo-javaagent 工程,目录结构如下:,Java agent超详细知识梳理,新建pom.xml​,引入javassist​用来修改目标类的字节码,增加自定义代码。通过maven-assembly-plugin插件打包自定义的 agent jar。,其中重点关注重点部分,编写 agent 核心代码 MethodAgentMain.java,我们使用了premain()​静态加载方式,agentmain​动态加载方式。并用到了Instrumentation​类结合javassist代码生成库进行字节码的修改。,编译打包:,执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:,Java agent超详细知识梳理,1.1.1 编写验证 agent 功能的测试类,创建 agent-example 工程,目录结构如下:,Java agent超详细知识梳理,编写测试 agent 功能的类 AgentTest.java,在 IDEA 的 Run/Debug Configurations 中,点击 Modify options,勾选上 add VM options,在 VM options 栏增加 -javaagent:/工程的父目录/demo-javaagent/demo-javaagent/target/demo-javaagent-1.0-jar-with-dependencies.jar,运行 Main.java 的 main 方法,可以看到控制台日志:,其中AgentTest.main start AgentTest.process start 等日志是我们自己写的 java agent 实现的功能,实现了方法运行开始和结束时打印日志。,动态加载不是通过 -javaagent: 的方式实现,而是通过 Attach API 的方式。,编写调用 Attach API 的测试类,先直接运行 org.example.agent.AgentTest#main​,注意不用加 -javaagent: 启动参数。,约 15 秒后,再运行 org.example.agent.AttachMain#main​,可以看到 org.example.agent.AttachMain#main 打印的日志:,之后可以看到 org.example.agent.AgentTest#main打印的日志中多了记录方法运行开始和结束的内容。,可以看到静态加载或动态加载相同的 agent,都能实现了记录记录方法运行开始和结束日志的功能。,我们可以稍微扩展一下,打印方法的入参、返回值,也可以实现替换 class,实现热加载的功能。,Instrumentation是 Java 提供的 JVM 接口,该接口提供了一系列查看和操作 Java 类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入 jar 文件等。使得开发者可以通过 Java 语言来操作和监控 JVM 内部的一些状态,进而实现 Java 程序的监控分析,甚至实现一些特殊功能(如 AOP、热部署)。,Instrumentation 的一些主要方法如下:,其中最常用的方法是addTransformer(ClassFileTransformer transformer)​,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:,addTransformer​方法配置之后,后续的类加载都会被Transformer拦截。,对于已经加载过的类,可以执行retransformClasses​来重新触发这个Transformer​的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。,在运行时,我们可以通过Instrumentation的redefineClasses​方法进行类重定义,在redefineClasses方法上有一段注释需要特别注意:,这里面提到,我们不可以增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。认识到这一点很重要,当我们通过 ASM 获取到增强的字节码之后,如果增强后的字节码没有遵守这些规则,那么调用redefineClasses方法来进行类的重定义就会失败。,主流的 JVM 都提供了Instrumentation​的实现,但是鉴于Instrumentation​的特殊功能,并不适合直接提供在 JDK 的runtime里,而更适合出现在 Java 程序的外层,以上帝视角在合适的时机出现。,因此如果想使用Instrumentation功能,拿到 Instrumentation 实例,我们必须通过 Java agent。,Java agent​是一种特殊的 Java 程序(Jar 文件),它是Instrumentation​的客户端。与普通 Java 程序通过 main 方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个 Java 应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。,Java agent与Instrumentation​密不可分,二者也需要在一起使用。因为Instrumentation​的实例会作为参数注入到Java agent的启动方法中。,Java agent 以 jar 包的形式部署在 JVM 中,jar 文件的 manifest 需要指定 agent 的类名。根据不同的启动时机,agent 类需要实现不同的方法(二选一)。,(1) JVM 启动时加载,JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。,(2) JVM 运行时加载,与 premain()一致,JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。,可以通过 maven plugin 配置,示例:,生成的 MANIFEST.MF,示例:,Java agent 的包先会被加入到 system class path 中,然后 agent 的类会被system calss loader(默认AppClassLoader)所加载,和应用代码的真实 classLoader 无关。例如:,当启动参数加上-javaagent:my-agent.jar​运行 SpringBoot 打包的 fatjar 时,fatjar 中应用代码和 lib 中的嵌套 jar 是由 org.springframework.boot.loader.LaunchedURLClassLoader​ 加载,但这个 my-agent.jar 依然是在system calss loader(默认AppClassLoader)​中加载,而非 org.springframework.boot.loader.LaunchedURLClassLoader 加载。,该类加载逻辑非常重要,在使用 Java agent 时如果遇到ClassNotFoundException、NoClassDefFoundError,很大可能就是与该加载逻辑有关。,Java agent 支持静态加载和动态加载。,2.2.1 静态加载 Java agent,静态加载,即 JVM 启动时加载,对应的是 premain()​ 方法。通过 vm 启动参数-javaagent将 agent jar 挂载到目标 JVM 程序,随目标 JVM 程序一起启动。,(1) -javaagent 启动参数,示例: 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 则无问题。,(2) premain()方法,注册类的ClassFileTransformer,在类加载的时候会自动更新对应的类的字节码,写法示例:,(3) 静态加载执行流程,agent 中的 class 由 system calss loader(默认AppClassLoader) 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI 的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发 ClassFileTransformer.transform(),最终修改 class 的字节码。,Java agent超详细知识梳理,(4) ClassFileTransformer.transform(),ClassFileTransformer.transform() 和 ClassLoader.load()的关系,下面是一次 ClassFileTransformer.transform()执行时的方法调用栈,,可以看到 ClassLoader.load()​加载类时,ClassLoader.load()​会调用ClassLoader.findClass(),ClassLoader.findClass()​会调用ClassLoader.defefineClass(),ClassLoader.defefineClass()​最终会执行ClassFileTransformer.transform(),ClassFileTransformer.transform()​可以对类进行修改。所以ClassLoader.load()最终加载 agent 修改后 Class 对象。,下面是精简后的 ClassLoader.load() 核心代码:,ClassFileTransformer.transform() 和 字节码增强,ClassFileTransformer.transform()​ 中可以对指定的类进行增强,我们可以选择的代码生成库修改字节码对类进行增强,比如ASM​, CGLIB​, Byte Buddy​, Javassist。,2.2.2 动态加载 Java agent,静态加载,即 JVM 启动后的任意时间点(即运行时),通过Attach API​动态地加载 Java agent,对应的是 agentmain() 方法。,Attach API部分将在后面的章节进行说明。,agentmain()方法,对于 VM 启动后加载的Java agent​,其agentmain()​方法会在加载之时立即执行。如果agentmain执行失败或抛出异常,JVM 会忽略掉错误,不会影响到正在 running 的 Java 程序。,一般 agentmain() 中会编写如下步骤:,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),启动时(Agent_OnLoad)和运行时 Attach(Agent_OnAttach),Instumentation API 可以支持 Java 语言实现 agent 功能,但是 JVMTI 功能比 Instumentation API 更强大,它支持:,获取所有线程、查看线程状态、线程调用栈、查看线程组、中断线程、查看线程持有和等待的锁、获取线程的 CPU 时间、甚至将一个运行中的方法强制返回值……,Java agent 是基于 JVMTI 实现,核心部分是 ClassFileLoadHook和TransFormClassFile。,ClassFileLoadHook​是一个 JVMTI 事件,该事件是 Instrumentation agent 的一个核心事件,主要是在读取字节码文件回调时调用,内部调用了TransFormClassFile的函数。,TransFormClassFile​的主要作用是调用java.lang.instrument.ClassFileTransformer的tranform​方法,该方法由开发者实现,通过Instrumentation的addTransformer方法进行注册。,在字节码文件加载的时候,会触发ClassFileLoadHook​事件,该事件调用TransFormClassFile​,通过经由Instrumentation​ 的 addTransformer 注册的方法完成整体的字节码修改。,对于已加载的类,需要调用retransformClass​函数,然后经由redefineClasses​函数,在读取已加载的字节码文件后,若该字节码文件对应的类关注了ClassFileLoadHook​事件,则调用ClassFileLoadHook事件。后续流程与类加载时字节码替换一致。,前文提到,Java agent 动态加载是通过 Attach API 实现。,Attach 机制是 JVM 提供一种 JVM 进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。,日常很多工作都是通过 Attach API 实现的,示例:,JDK 自带的一些命令,如:jstack 打印线程栈、jps 列出 Java 进程、jmap 做内存 dump 等功能,Arthas、Greys、btrace 等监控诊断产品,通过 attach 目标 JVM 进程发送指定命令,可以实现方法调用等方面的监控。,由于是进程间通讯,那代表着使用 Attach API 的程序需要是一个独立的 Java 程序,通过 attach 目标进程,与其进行通讯。下面的代码表示了向进程 pid 为 1234 的 JVM 发起通讯,加载一个名为 agent.jar 的 Java agent。,vm.loadAgent 之后,相应的 agent 就会被目标 JVM 进程加载,并执行 agentmain() 方法。,执行的流程图:,Java agent超详细知识梳理,以 Hotspot 虚拟机,Linux 系统为例。当 external process(attach 发起的进程)执行 VirtualMachine.attach 时,需要通过操作系统提供的进程通信方法,例如信号、socket,进行握手和通信。其具体内部实现流程如下所示:,Java agent超详细知识梳理,上面提到了两个文件:,VirtualMachine.attach 动作类似 TCP 创建连接的三次握手,目的就是搭建 attach 通信的连接。而后面执行的操作,例如 vm.loadAgent,其实就是向这个 socket 写入数据流,接收方 target VM 会针对不同的传入数据来做不同的处理。,作者介绍,原文作者:Spurs 蒋,原文链接:https://juejin.cn/post/7157684112122183693,本文转载自微信公众号「架构染色」,可以通过以下二维码关注。转载本文请联系【架构染色】公众号作者。,Java agent超详细知识梳理

© 版权声明

相关文章