什么是JVM系列的javaagent

JVM源码分析篇 | javaagent原理深度解读 本系列文章都是基于Hotspot/JDK源码,从源码角度来分析我们常见的JVM参数,Java概念以及对应的实现原理及玩法等,希望从根本上来理清Java知识点,我们会不定期地分享这个系列的文章,这些文章可能源于最近碰到的问题,也可能是同学们的提问,甚至有可能是我们突然想到的话题等,有些东西我们现在可能也不一定清楚,但是我们非常愿意花时间去了解清楚并分享给大家。 本文重点讲述javaagent的具体实现,因为它面向的是我们java程序员,而且agent都是用java编写的,不需要太多的c/c++编程基础,不过 阅读详情

一、开篇

在梳理SkyWalking agent的plugin、elasticsearch的plugin、arthas等技术的原理时,发现他们的底层原理很多是相同的。这类工具都用到了javaagent、类加载、类隔离等技术,在此进行归类梳理。

本篇将梳理Java agent相关内容,在此先把这些技术整体的关系梳理如下:

​

二、什么是Java agent

Java agent 是从 JDK1.5开始引入的技术,支持将 jar 依附在 JVM 进程中运行。

Java agent 分为静态加载和动态加载两种agent加载的方式,如下图:

​

Java agent 技术结合 Java Intrumentation API 可以实现类修改、热加载等功能,下面是 Java agent 技术的常见应用场景:

​

三、Java agent 示例

我们先用一个 Java agent 实现方法开始和结束时打印日志的简单例子来实践一下,通过示例,可以很快对后面 Java agent 技术有初步的理解。

1 Java agent 实现方法开始和结束时打印日志

1.1 开发 agent

创建 demo-javaagent 工程,目录结构如下:

​

新建pom.xml,引入javassist用来修改目标类的字节码,增加自定义代码。通过maven-assembly-plugin插件打包自定义的 agent jar。

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>demo-javaagent</artifactId> <version>1.0</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.25.0-GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.1</version> <configuration> <descriptorRefs> <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar--> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <!-- 设置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> </archive> </configuration> <executions> <execution> <!--绑定到package生命周期阶段上--> <phase>package</phase> <goals> <!--绑定到package生命周期阶段上--> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> </configuration> </plugin> </plugins> </build> </project> 复制代码

其中重点关注重点部分

<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> 复制代码

编写 agent 核心代码 MethodAgentMain.java,我们使用了premain()静态加载方式,agentmain动态加载方式。并用到了Instrumentation类结合javassist代码生成库进行字节码的修改。

package demo; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; public class MethodAgentMain { /** 被转换的类 */ public static final String TRANSFORM_CLASS = "org.example.agent.AgentTest"; /** 静态加载。Java agent指定的premain方法,会在main方法之前被调用 */ public static void premain(String args, Instrumentation instrumentation) { System.out.println("premain start!"); addTransformer(instrumentation); System.out.println("premain end!"); } /** 动态加载。Java agent指定的premain方法,会在main方法之前被调用 */ public static void agentmain(String args, Instrumentation instrumentation) { System.out.println("agentmain start!"); addTransformer(instrumentation); Class<?>[] classes = instrumentation.getAllLoadedClasses(); if (classes != null){ for (Class<?> c: classes) { if (c.isInterface() ||c.isAnnotation() ||c.isArray() ||c.isEnum()){ continue; } if (c.getName().equals(TRANSFORM_CLASS)) { try { System.out.println("retransformClasses start, class: " + c.getName()); /* * retransformClasses()对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。 * retransformClasses()可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性 */ instrumentation.retransformClasses(c); System.out.println("retransformClasses end, class: " + c.getName()); } catch (UnmodifiableClassException e) { System.out.println("retransformClasses error, class: " + c.getName() + ", ex:" + e); e.printStackTrace(); } } } } System.out.println("agentmain end!"); } private static void addTransformer (Instrumentation instrumentation) { /* Instrumentation提供的addTransformer方法,在类加载时会回调ClassFileTransformer接口 */ instrumentation.addTransformer(new ClassFileTransformer() { public byte[] transform(ClassLoader l,String className, Class<?> c,ProtectionDomain pd, byte[] b){ try { className = className.replace("/", "."); if (className.equals(TRANSFORM_CLASS)) { final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get(TRANSFORM_CLASS); for (CtMethod method : clazz.getMethods()) { /* * Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报 * javassist.CannotCompileException: no method body at javassist.CtBehavior.addLocalVariable() * 报错原因如下 * 来自Stack Overflow网友解答 * Native methods cannot be instrumented because they have no bytecodes. * However if native method prefix is supported ( Transformer.isNativeMethodPrefixSupported() ) * then you can use Transformer.setNativeMethodPrefix() to wrap a native method call inside a non-native call * which can then be instrumented */ if (Modifier.isNative(method.getModifiers())) { continue; } method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "." + method.getName() + " start.\");"); method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "." + method.getName() + " end.\");", false); } return clazz.toBytecode(); } } catch (Exception e) { e.printStackTrace(); } return null; } }, true); } } 复制代码

编译打包:

执行 mvn clean package 编译打包,最终打包生成了 agent jar 包,结果示例:

​

1.1.1 编写验证 agent 功能的测试类

创建 agent-example工程,目录结构如下:

​编写测试 agent 功能的类 AgentTest.java

package org.example.agent; public class AgentTest { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { System.out.println("process result: " + process()); Thread.sleep(5000); } } public static String process() { System.out.println("process!"); return "success"; } } 复制代码

1.2 使用 java agent 静态加载方式实现

在 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 方法,可以看到控制台日志:

premain start! premain end! AgentTest.main start. AgentTest.process start. process! AgentTest.process end. process result: success AgentTest.process start. process! AgentTest.process end. .......省略重复的部分...... 复制代码

其中AgentTest.main start AgentTest.process start 等日志是我们自己写的 java agent 实现的功能,实现了方法运行开始和结束时打印日志。

1.3 使用 java agent 动态加载方式实现

动态加载不是通过 -javaagent: 的方式实现,而是通过 Attach API 的方式。

编写调用 Attach API 的测试类

package org.example.agent; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; public class AttachMain { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> listBefore = VirtualMachine.list(); // agentmain()方法所在jar包 String jar = "/Users/terry/Gits/agent/java-agent-group/demo-javaagent/demo-javaagent/target/demo-javaagent-1.0-jar-with-dependencies.jar"; for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) { // 针对指定名称的JVM实例 if (virtualMachineDescriptor.displayName().equals("org.example.agent.AgentTest")) { System.out.println("将对该进程的vm进行增强:org.example.agent.AgentTest的vm进程, pid=" + virtualMachineDescriptor.id()); // attach到新JVM VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor); // 加载agentmain所在的jar包 vm.loadAgent(jar); // detach vm.detach(); } } } } 复制代码

先直接运行 org.example.agent.AgentTest#main,注意不用加 -javaagent: 启动参数。

process! process result: success process! process result: success process! process result: success .......省略重复的部分...... 复制代码

约15秒后,再运行 org.example.agent.AttachMain#main,可以看到 org.example.agent.AttachMain#main 打印的日志:

找到了org.example.agent.AgentTest的vm进程, pid=67398 复制代码

之后可以看到 org.example.agent.AgentTest#main打印的日志中多了记录方法运行开始和结束的内容。

.......省略重复的部分...... process! process result: success agentmain start! process! process result: success agentmain end! process! process result: success process! process result: success .......省略重复的部分...... 复制代码

1.4 小结

可以看到静态加载或动态加载相同的 agent,都能实现了记录记录方法运行开始和结束日志的功能。

我们可以稍微扩展一下,打印方法的入参、返回值,也可以实现替换 class,实现热加载的功能。

四、Instrumentation

1 Instrumentation API 介绍

Instrumentation是Java提供的JVM接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)。

Instrumentation的一些主要方法如下:

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; } 复制代码

addTransformer方法配置之后,后续的类加载都会被Transformer拦截。

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

2 Instrumentation的局限性

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

* The redefinition may change method bodies, the constant pool and attributes. * The redefinition must not add, remove or rename fields or methods, change the * signatures of methods, or change inheritance. These restrictions maybe be * lifted in future versions. The class file bytes are not checked, verified and installed * until after the transformations have been applied, if the resultant bytes are in * error this method will throw an exception. 复制代码

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

五、Java agent

主流的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的启动方法中。

1 Java agent 的格式

1.1 premain和agentmain

Java agent以jar包的形式部署在JVM中,jar文件的manifest需要指定agent的类名。根据不同的启动时机,agent类需要实现不同的方法(二选一)。

(1) JVM 启动时加载

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

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

(2) JVM 运行时加载

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

与 premain()一致,JVM将首先寻找[1],如果没有发现[1],再寻找[2]。

1.2 指定MANIFEST.MF

可以通过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: terry Agent-Class: demo.MethodAgentMain Can-Redefine-Classes: true Can-Retransform-Classes: true 复制代码

2 Java agent 的加载

2.1 Java agent 与 ClassLoader

Java agent 的包先会被加入到 system class path 中,然后 agent 的类会被SystemClassLoader(AppClassLoader)所加载,和应用代码的真实classLoader无关。例如:当启动参数加上-javaagent:my-agent.jar运行 SpringBoot 打包的 fatjar 时,fatjar 中应用代码和 lib 中嵌套 jar 是由 org.springframework.boot.loader.LaunchedURLClassLoader 加载,但这个 my-agent.jar 依然是在SystemClassLoader(AppClassLoader)中加载,而非 org.springframework.boot.loader.LaunchedURLClassLoader 加载。

该类加载逻辑非常重要,在使用 Java agent 时如果遇到ClassNotFoundException、NoClassDefFoundError,很大可能就是与该加载逻辑有关。

2.2 静态加载、动态加载 Java agent

Java agent 支持静态加载和动态加载。

2.2.1 静态加载 Java agent

静态加载,即 JVM 启动时加载,对应的是 premain() 方法。通过 vm 启动参数-javaagent将 agent jar 挂载到目标 JVM 程序,随目标 JVM 程序一起启动。

(1) -javaagent启动参数

其中 -javaagent格式: "-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。

(2) premain()方法

  • premain()方法会在程序main方法执行之前被调用,此时大部分Java类都没有被加载("大部分"是因为,agent类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。

  • 如果此时premain方法执行失败或抛出异常,那么JVM的启动会被终止。

  • premain() 中一般会编写如下步骤:

  • 注册类的 ClassFileTransformer,在类加载的时候会自动更新对应的类的字节码

  • 写法示例:

// Java agent指定的premain方法,会在main方法之前被调用 public static void premain(String args, Instrumentation inst) { // Instrumentation提供的addTransformer方法,在类加载时会回调ClassFileTransformer接口 inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // TODO 字节码修改 byte[] transformed = null; return transformed; } }); } 复制代码

(3) 静态加载执行流程

agent 中的 class 由 AppClassLoader 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发ClassFileTransformer.transform(),最终修改 class 的字节码。

![image-20221020203356897](/Users/terry/Library/Application Support/typora-user-images/image-20221020203356897.png)

(4) ClassFileTransformer.transform()

ClassFileTransformer.transform() 和 ClassLoader.load()的关系

下面是一次 ClassFileTransformer.transform()执行时的方法调用栈,

transform:38, MethodAgentMain$1 (demo) transform:188, TransformerManager (sun.instrument) transform:428, InstrumentationImpl (sun.instrument) defineClass1:-1, ClassLoader (java.lang) defineClass:760, ClassLoader (java.lang) defineClass:142, SecureClassLoader (java.security) defineClass:467, URLClassLoader (java.net) access$100:73, URLClassLoader (java.net) run:368, URLClassLoader$1 (java.net) run:362, URLClassLoader$1 (java.net) doPrivileged:-1, AccessController (java.security) findClass:361, URLClassLoader (java.net) loadClass:424, ClassLoader (java.lang) loadClass:331, Launcher$AppClassLoader (sun.misc) loadClass:357, ClassLoader (java.lang) checkAndLoadMain:495, LauncherHelper (sun.launcher) 复制代码

可以看到 ClassLoader.load()加载类时,ClassLoader.load()会调用ClassLoader.findClass(),ClassLoader.findClass()会调用ClassLoader.defefineClass(),ClassLoader.defefineClass()最终会执行ClassFileTransformer.transform() ,ClassFileTransformer.transform() 可以对类进行修改。所以ClassLoader.load()最终加载 agent 修改后Class对象。

下面是精简后的 ClassLoader.load() 核心代码:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 判断是否已经加载过了,如果没有,则进行load // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // findClass()内部最终会调用 Java agent 中 ClassFileTransformer.transform() c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } 复制代码

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() 中会编写如下步骤:

  • 注册类的 ClassFileTransformer

  • 调用 retransformClasses 方法对指定的类进行重加载

六、JVMTI

1 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)

2 加载时机

启动时(Agent_OnLoad)和运行时Attach(Agent_OnAttach)

3 功能

Instumentation API 可以支持 Java 语言实现 agent 功能,但是 JVMTI 功能比 Instumentation API 更强大,它支持:

  • 获取所有线程、查看线程状态、线程调用栈、查看线程组、中断线程、查看线程持有和等待的锁、获取线程的CPU时间、甚至将一个运行中的方法强制返回值……

  • 获取Class、Method、Field的各种信息,类的详细信息、方法体的字节码和行号、向Bootstrap/System Class Loader添加jar、修改System Property……

  • 堆内存的遍历和对象获取、获取局部变量的值、监测成员变量的值……

  • 各种事件的callback函数,事件包括:类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、gc开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出……

  • 设置与取消断点、监听断点进入事件、单步执行事件……

4 JVMTI 与 Java agent

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事件。后续流程与类加载时字节码替换一致。

七、Attach API

前文提到,Java agent 动态加载是通过 Attach API 实现。

1 Attach API 介绍

Attach机制是JVM提供一种JVM进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。

日常很多工作都是通过 Attach API 实现的,示例:

  • JDK 自带的一些命令,如:jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能

  • Arthas、Greys、btrace 等监控诊断产品,通过 attach 目标 JVM 进程发送指定命令,可以实现方法调用等方面的监控。

2 Attach API 用法

由于是进程间通讯,那代表着使用Attach API的程序需要是一个独立的Java程序,通过attach目标进程,与其进行通讯。下面的代码表示了向进程pid为1234的JVM发起通讯,加载一个名为agent.jar的Java agent。

// VirtualMachine等相关Class位于JDK的tools.jar VirtualMachine vm = VirtualMachine.attach("1234"); // 1234表示目标JVM进程pid try { vm.loadAgent(".../javaagent.jar"); // 指定agent的jar包路径,发送给目标进程 } finally { // attach 动作的相反的行为,从 JVM 上面解除一个代理 vm.detach(); } 复制代码

vm.loadAgent 之后,相应的 agent 就会被目标 JVM 进程加载,并执行 agentmain() 方法。

执行的流程图:

​

3 Attach API 原理

以Hotspot虚拟机,Linux系统为例。当external process(attach发起的进程)执行VirtualMachine.attach时,需要通过操作系统提供的进程通信方法,例如信号、socket,进行握手和通信。其具体内部实现流程如下所示:

 

上面提到了两个文件:

  • .attach_pidXXX 后面的XXX代表pid,例如pid为1234则文件名为.attach_pid1234。该文件目的是给目标JVM一个标记,表示触发SIGQUIT信号的是attach请求。这样目标JVM才可以把SIGQUIT信号当做attach连接请求,再来做初始化。其默认全路径为/proc/XXX/cwd/.attach_pidXXX,若创建失败则使用/tmp/attach_pidXXX

  • .java_pidXXX 后面的XXX代表pid,例如pid为1234则文件名为.java_pid1234。由于Unix domain socket通讯是基于文件的,该文件就是表示external process与target VM进行socket通信所使用的文件,如果存在说明目标JVM已经做好连接准备。其默认全路径为/proc/XXX/cwd/.java_pidXXX,若创建失败则使用/tmp/java_pidXXX

  VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。

JVM——JavaAgent与字节码注入 阅读详情

相关推荐

JVM源码分析之javaagent原理完全解读

概述 本文重点讲述javaagent的具体实现,因为它面向的是我们java程序员,而且agent都是用java编写的,不需要太多的c/c++编程基础,不过这篇文章里也会讲到JVMTIAgent(c实现的),因为javaagent的运行还是依赖于一个特殊的JVMTIAgent。 对于javaagent或许大家都听过,甚至使用过,常见的用法大致如下: java -javaagent:myagent.j...

HeapDump性能社区的博客 1175

java agent简介

笼统地来讲,Java Agent 是一个统称,该功能是 Java 虚拟机提供的一整套后门,通过这套后门可以对虚拟机方方面面进行监控与分析,甚至干预虚拟机的运行。JDK 从5.0开始,提供了一个名为的工具包:1.jpg借助该包,开发者可以构建一个独立于应用程序的代理(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够动态替换和修改某些类的定义。这样的特性实际上提供了一种虚拟机级别的 AOP 实现。事实上, 包是基于JVMTI机制实现的:Instrument 就是一个基于 JVMTI 接口的,以代理

Java技术江湖 1万+

深入理解Java Agent:让你的Java程序优雅地做“间谍”

Java Agent是一种特殊的Java程序,运行在目标Java应用前或运行时,用来修改目标应用的字节码。借助于Java Instrumentation API,Java Agent可以插入字节码转换,使得我们可以在不修改源代码的前提下就能够监控和增强程序的行为。Java Agent为我们提供了一种强大且灵活的机制,能够在不改变源码的情况下,实现对Java程序的监控、调试和增强。虽然它带来了很多便利,但也需要小心使用,以避免引入不必要的问题。

fudaihb的博客 2761

java agent技术原理及简单实现

注:本文定义-在函数执行前后增加对应的逻辑的操作统称为MOCK 1、引子 在某天与QA同学进行沟通时,发现QA同学有针对某个方法调用时,有让该方法停止一段时间的需求,我对这部分的功能实现非常好奇,因此决定对原理进行一些深入的了解,力争找到一种使用者尽可能少的对原有代码进行修改的方式,以达到对应的MOCK要求。 整体的感知程度可以分为三个级别: 硬编码 增加配置 无需任何修改 2、思路 在对方法进行mock,暂停以及异常模拟,在不知道其原理的情况下,进行猜想,思考其具体的

hellozhxy的博客 1万+

Java agent 使用详解

Java agent 使用详解

congge 8982

【项目实战】在Java应用程序中,Agent可以分为两类:Java Agent(修改字节码)和Native Agent(使用操作系统API)

Java Agent是一个Java程序,它可以在JVM启动时通过JVM参数加载到JVM中。

奶爸的编程之路,也就一周冷个三天 1227

基于JavaAgent的全链路监控一《嗨!JavaAgent》

前言介绍 全链路监控又名分布式监控系统全链路追踪,目前市面的全链路监控系统基本都是参考Google的Dapper(大规模分布式系统的跟踪系统)来做的。例如;蚂蚁金服分布式链路跟踪组件SOFATracer、Gokit微服务-服务链路追踪 、Pinpoint、Prometheus(普罗米修斯)等等。 章节列表 | 关注微信公众号,bugstack虫洞栈,回复<基于JavaAgent的全链路...

bugstack虫洞栈 4756

基于STM32的频率计(采用外部中断的方式)

前言 最近在了解电赛相关的东西,和指导老师交流一下,我决定先从最简单的频率计做起。 频率计是来检测信号的频率的,在做频率计的时候可以用信号发生器发出的信号来进行测试。 原理 这是最基础的方波,我们可以通过检测上升沿的方式来测量其频率。从周期的角度出发连续两个上升沿的时间就是这个方波的周期,那这个周期分之一就是这个方波的频率了。 在思考了一段时间~~(我是萌新。。)~~ 我觉得有两个方法可以实现,第一个是采用stm32定时器的输入捕获功能,第二个就是用外部中断来实现这个功能。正如标题,我这次采用外部中断的方

qq_45784922的博客 6171

偷天换日,用JavaAgent欺骗你的JVM

并且,在redefineClasses方法重定义的过程中,传入的是ClassDefinition的数组,它会按照这个数组顺序进行加载,以便满足在类之间相互依赖的情况下进行更改。因为addTransformer方法不能指明需要转换的类,所以需要通过className判断当前加载的class是否我们要拦截的目标class,对于非目标class直接返回原字节数组,注意className的格式,需要将类全限定名中的.替换为/在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。

2303_77877769的博客 1094

Unity 机械臂控制(二)——从碰撞检测到姿态解算:实现精准抓取

本文详细介绍了Unity中机械臂控制的实现方法,从碰撞检测到姿态解算,涵盖精准抓取的全流程。重点讲解了碰撞检测的分层处理、状态机设计、逆向运动学(IK)算法以及抓取稳定性优化等关键技术,并提供了实用的调试技巧和性能优化建议,帮助开发者实现高效的机械臂控制。

weixin_28693447的博客 307

手把手教学|用飞书+扣子工作流实现自动化批量图片生成,效率提升99%!体验完整操作流程

在先前的文章中,我们已经深入探讨了扣子智能体的多种应用场景,以及如何创建和使用工作流进行高效管理。本文将引领大家通过一个具体案例,将扣子的工作流功能嵌入飞书应用中,实现操作的自动化与高效化。

weixin_66485800的博客 6030

Javaagent使用指南

JVM启动前静态Instrument Javaagent是什么? Javaagent是java命令的一个参数。参数javaagent可以用于指定一个jar包,并且对该java包有2个要求: 这个jar包的MANIFEST.MF文件必须指定Premain-Class项。 Premain-Class指定的那个类必须实现premain()方法。 premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java虚拟机启动时,在执行 main 函数之前,JVM会先运行-javaagent所指定

qq_31279701的博客 1万+

机器学习ROC曲线中的阈值thresholds

在ROC曲线上的不同点对应于不同的阈值。根据任务的具体要求,可以选择合适的阈值,以达到平衡召回率和误报率,或者根据具体应用场景调整模型的工作点。,表示样本属于正例的概率。这个概率值可以被截断为二元分类的决策,通过选择一个阈值来决定样本是被分类为正例还是负例。通过改变阈值,可以得到不同的真正例率(TPR)和假正例率(FPR),从而绘制出ROC曲线。总体来说,ROC曲线提供了模型在不同阈值下的性能综合情况,帮助分析模型的分类能力。通常来说,随着阈值的增加,TPR会减小,而FPR会增加。ROC曲线的绘制涉及。

行子的博客 3657

javaagent使用指南

今天打算写一下 Javaagent,一开始我对它的概念也比较陌生,后来在别人口中听到 字节码插桩,bTrace,Arthas后面才逐渐了解到Java还提供了这么个工具。 JVM启动前静态Instrument Javaagent 是什么? Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求: 这个 jar 包的 MA...

rickiyang的博客 3230

SAP 管道业务(Pipeline Procurement)是用于水、电、气等不间断供应且无库存管理的物料采购场景,核心是消耗即记账、无库存持有,通过 MM-FI 集成实现实时负债与成本过账

摘要:SAP管道业务适用于水电气等无库存管理的物料采购场景,通过消耗即记账机制实现实时负债与成本过账。核心配置包括创建PIPE物料类型(无库存管理)、维护管道采购信息记录、配置自动记账科目等。SAP PO可集成外部系统实现流程自动化,如自动过账提取数据、发票校验等。实施要点在于物料主数据、信息记录和科目确定的精准配置,以及PO接口的可靠对接,最终构建端到端的高效采购与财务结算体系。

金牌架构师 749

Modbus RTU/TCP协议栈手写实现:主从站设计、功能码、CRC、异常响应

本文从协议栈架构、帧结构、功能码、CRC校验、异常响应、主从站设计等多个维度,完整阐述了Modbus RTU/TCP协议栈的手写实现方案。帧间隔机制是RTU的核心,T3.5超时检测决定了帧边界识别CRC16查表法以512字节ROM换取8倍性能提升,适合嵌入式功能码路由表实现可扩展的代码架构,便于新增功能异常响应机制保障协议健壮性,每个错误都有明确反馈Modbus TCP在RTU基础上增加MBAP头,实现以太网扩展多核RTOS环境下的并发访问与线程安全RS-485总线的电气隔离与浪涌保护。

从事移动开发八年,csdn博客专家,阿里云特邀专家,华为云云享专家,高级网络信息安全工程师,大数据工程师(高级),python技术开发(高级),HarmonyOS 应用开发者高级认证,现在某学校任教。 1万+

基于JavaAgent的全链路监控四《JVM内存与GC信息》

章节列表 | 关注微信公众号,bugstack虫洞栈,回复<基于JavaAgent的全链路监控>获取源码 基于JavaAgent的全链路监控一《嗨!JavaAgent》 基于JavaAgent的全链路监控二《通过字节码增加监控执行耗时》 基于JavaAgent的全链路监控三《ByteBuddy操作监控方法字节码》 基于JavaAgent的全链路监控四《JVM内存与GC信息》 基于Ja...

bugstack虫洞栈 2430

手把手教你 Ubuntu 24.04 离线安装 Ollama

本文介绍 Ubuntu 下离线安装 Ollama 并使用 Ollama 部署 llama3.1-70b 大模型

举世誉之而不加劝,举世非之而不加沮,定乎内外之分,辩乎荣辱之境,斯已矣。 1021

Maven项目管理之javaagent使用代理jar包向JVM进程传参

文章目录1. javaagent代理包配置2. 代理包的运行3. 解决无法加载org.slf4j.impl.StaticLoggerBinder的错误 最近在学习使用一个开源的关系提取工具TCtracer,在使用过程中需要利用打包好的jar包向JVM进程中传递参数,因此就此记录一下流程和过程中遇到的问题及解决方案。 1. javaagent代理包配置 在IDEA界面有一个Edit Configurations,点击然后按界面左上角的+号: 在Modify options中选中Add VM options

weixin_36488653的博客 2049

YOLOv11改进 | 检测头篇 | 利用动态蛇形卷积DySnakeConv改进yolov11分割检测头(全网独家首发,Segment)

本文给大家带来的最新改进机制是一种我进行优化的专用于分割的检测头,在分割的过程中,最困难的无非就是边缘的检测,动态蛇形卷积(Dynamic Snake Convolution)通过自适应地聚焦于细长和迂回的局部结构,准确地捕捉管状结构的特征。这种卷积方法的核心思想是,通过动态形状的卷积核来增强感知能力,针对管状结构的特征提取进行优化,所以将这个卷积针对于YOLOv11的分割头进行融合是非常合适的,当然本文的检测头也支持用于目标检测,但是我将其设计出来是主要为了分割的读者使用的。

Snu77的博客 3433

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

javaagent技术原理

前言 说道Javaagent是最近经常在使用这个技术,顺便了解了原理与根源,实际上就是jvm开个代理字节码修改的instrument接口。但实际上使用,根据使用的方式不同而略有区别。 1. Javaagent使用 实际上,笔者在前段时间写了arthas的启动原理(83条消息) arthas 启动原理分析_fenglllle的博客-优快云博客,简单的说明了Javaagent的2种方式,jvm参数方式与动态attach。 2. Javaagent原理 3. idea debug 总结 ..

fenglllle的博客 4733

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

浅谈JavaAgent

Javaagent是java命令的一个参数。premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法。

Sentiment的博客 2430

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

JavaAgent技术应用和原理:JVM持久化监控

背景字节码增强技术字节码增强:Java Agent通过修改字节码来实现对应用程序的增强,例如添加日志、性能监控、事务管理等。工具:常用的字节码增强工具包括ASM、Javassist、Byte Buddy等。JavaAgent技术基于JVM工具接口(JVMTI),通过字节码插桩实现其功能,字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。JavaAgentJavaAgent...

Diligence is the mother of success. 1456

一个简单JavaAgent的实现

一、什么是javaagent javaagent是一个JVM“插件”,一种专门精心制作的.jar文件,它能够利用JVM提供的Instrumentation API。 1.1、概要 Java Agent由三部分组成:代理类、代理类元信息和JVM加载.jar和代理的机制,整体内容如下图所示: 1.2、javaagent的基石 java.lang.instrument 为javaagent 通过修改方...

Davis 5768

JVM参数中的 -、-X、-XX、-D、-Javaagent:

一.JVM 启动参数规则:-、-X、-XX、-D 参考:https://blog.youkuaiyun.com/liujianyangbj/article/details/115215680 通过JVM运行一个java程序的时候,我们通常会用java -jar xxxx.jar 的命令,我们通常都会根据情况配置很多的运行参数。这些参数可以分为四类: 1.以 - 开头的是标准参数,所有JVM都支持,并且向后兼容 -verbose:gc -server 2.以 -X开头的为非标准参数,绝大多数都是控制JVM的参数 -X

lk的博客 2653
上一篇: 帮助你深入理解JVM虚拟机,什么是Java内存模型结构的方法区
下一篇: 都2202年了,还有人在Java多线程批量操作的时候不做事务控制?
Java_LingFeng
博客等级 码龄4年 505粉丝 259原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值