一、简介

1 开篇

在整理SkyWalking agentpluginelasticsearchpluginarthas等技能的原理时,发现他们的底层原理许多是相同的。这类工具都用到了Java agent、类加载、类阻隔等技能,在此进行归类整理。

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

JVM系列-Java agent超详细知识梳理

二、 Java agent 运用场景

Java agent 技能结合 Java Intrumentation API 能够完结类修正、热加载等功用,下面是 Java agent 技能的常见运用场景:

JVM系列-Java agent超详细知识梳理

三、Java agent 示例

咱们先用一个 Java agent 完结办法开端和完毕时打印日志的简略例子来实践一下,经过示例,能够很快对后边 Java agent 技能有开端的理解

1 Java agent 完结办法开端和完毕时打印日志

1.1 开发 agent

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

JVM系列-Java agent超详细知识梳理

新建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 包,结果示例:

JVM系列-Java agent超详细知识梳理

1.1.1 编写验证 agent 功用的测验类

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

JVM系列-Java agent超详细知识梳理

编写测验 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的局限性

在运转时,咱们能够经过InstrumentationredefineClasses办法进行类重界说,在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 agentInstrumentation密不可分,二者也需求在一起运用。因为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 时假如遇到ClassNotFoundExceptionNoClassDefFoundError,很大或许便是与该加载逻辑有关。

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 对类的修正存在冲突,在实际项目中用到了pinpointSkyWalking的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 的字节码。

JVM系列-Java agent超详细知识梳理

(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 介绍

JVMTIJVM 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 来进行完结。

发动办法

JVMTIInstumentation 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 完结,中心部分是 ClassFileLoadHookTransFormClassFile

ClassFileLoadHook是一个 JVMTI 事情,该事情是 Instrumentation agent 的一个中心事情,首要是在读取字节码文件回调时调用,内部调用了TransFormClassFile的函数。

TransFormClassFile的首要效果是调用java.lang.instrument.ClassFileTransformertranform办法,该办法由开发者完结,经过InstrumentationaddTransformer办法进行注册。

在字节码文件加载的时分,会触发ClassFileLoadHook事情,该事情调用TransFormClassFile,经过经由InstrumentationaddTransformer 注册的办法完结全体的字节码修正。

关于已加载的类,需求调用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() 办法。

履行的流程图

JVM系列-Java agent超详细知识梳理

3 Attach API 原理

以Hotspot虚拟机,Linux系统为例。当external process(attach建议的进程)履行VirtualMachine.attach时,需求经过操作系统供给的进程通讯办法,例如信号、socket,进行握手和通讯。其详细内部完结流程如下所示:

JVM系列-Java agent超详细知识梳理

上面说到了两个文件:

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