JVM系列-Java agent超詳細知識梳理

語言: CN / TW / HK

一、簡介

1 開篇

在梳理SkyWalking agentpluginelasticsearchpluginarthas等技術的原理時,發現他們的底層原理很多是相同的。這類工具都用到了Java agent、類加載、類隔離等技術,在此進行歸類梳理。

本篇將梳理Java agent相關內容。在此先把這些技術整體的關係梳理如下:

image-20221027172216225

二、 Java agent 使用場景

Java agent 技術結合 Java Intrumentation API 可以實現類修改、熱加載等功能,下面是 Java agent 技術的常見應用場景:

image-20221023202542715

三、Java agent 示例

我們先用一個 Java agent 實現方法開始和結束時打印日誌的簡單例子來實踐一下,通過示例,可以很快對後面 Java agent 技術有初步的理解

1 Java agent 實現方法開始和結束時打印日誌

1.1 開發 agent

創建 demo-javaagent 工程,目錄結構如下:

image-20221023202607804

新建pom.xml,引入javassist用來修改目標類的字節碼,增加自定義代碼。通過maven-assembly-plugin插件打包自定義的 agent jar。

```xml

4.0.0

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

```

其中重點關注重點部分

```xml

demo.MethodAgentMain

demo.MethodAgentMain

true

true ```

編寫 agent 核心代碼 MethodAgentMain.java,我們使用了premain()靜態加載方式,agentmain動態加載方式。並用到了Instrumentation類結合javassist代碼生成庫進行字節碼的修改。

```java 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 包,結果示例:

image-20221023192124550

1.1.1 編寫驗證 agent 功能的測試類

創建 agent-example工程,目錄結構如下:

image-20221023192130854

編寫測試 agent 功能的類 AgentTest.java

```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 方法,可以看到控制枱日誌:

bash 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 的測試類

```java 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: 啟動參數。

java process! process result: success process! process result: success process! process result: success .......省略重複的部分......

約15秒後,再運行 org.example.agent.AttachMain#main,可以看到 org.example.agent.AttachMain#main 打印的日誌:

bash 找到了org.example.agent.AgentTest的vm進程, pid=67398

之後可以看到 org.example.agent.AgentTest#main打印的日誌中多了記錄方法運行開始和結束的內容。

bash .......省略重複的部分...... 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的一些主要方法如下:

```java 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接口,定義如下:

```java 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方法上有一段註釋需要特別注意:

java * 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 啟動時加載

java [1] public static void premain(String agentArgs, Instrumentation inst); [2] public static void premain(String agentArgs);

JVM將首先尋找[1],如果沒有發現[1],再尋找[2]。

(2) JVM 運行時加載

java [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配置,示例:

```xml

demo.MethodAgentMain demo.MethodAgentMain true true ```

生成的MANIFEST.MF,示例:

yaml 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 // 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 的字節碼。

image-20221020203356897

(4) ClassFileTransformer.transform()

ClassFileTransformer.transform() 和 ClassLoader.load()的關係

下面是一次 ClassFileTransformer.transform()執行時的方法調用棧,

java 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() 核心代碼:

```java 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。

java // 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() 方法。

執行的流程圖

img

3 Attach API 原理

以Hotspot虛擬機,Linux系統為例。當external process(attach發起的進程)執行VirtualMachine.attach時,需要通過操作系統提供的進程通信方法,例如信號、socket,進行握手和通信。其具體內部實現流程如下所示:

image-20221023232014110

上面提到了兩個文件:

  • .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會針對不同的傳入數據來做不同的處理。