2

Java agent超详细知识梳理

 1 year ago
source link: https://www.51cto.com/article/722419.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java agent超详细知识梳理

作者:Spurs 蒋 2022-11-10 07:38:56
在梳理SkyWalking agent的plugin、elasticsearch的plugin、arthas等技术的原理时,发现他们的底层原理很多是相同的。这类工具都用到了Java agent、类加载、类隔离等技术,在此进行归类梳理。

​一、简介

在梳理SkyWalking agent的plugin、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 技术有初步的理解。

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

1.1 开发 agent

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

图片

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

<?xml versinotallow="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 的类会被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,很大可能就是与该加载逻辑有关。

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 由 system calss loader(默认AppClassLoader) 加载,premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI 的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发 ClassFileTransformer.transform(),最终修改 class 的字节码。

图片

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

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 会针对不同的传入数据来做不同的处理。

  • 谈谈 Java Intrumentation 和相关应用
  • Java 动态调试技术原理及实践

原文作者:Spurs 蒋

原文链接:https://juejin.cn/post/7157684112122183693

本文转载自微信公众号「架构染色」,可以通过以下二维码关注。转载本文请联系【架构染色】公众号作者。

d802b506163530d81e6690363baead2c0eb4c3.jpg

责任编辑:武晓燕 来源: 架构染色

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK