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會針對不同的傳入資料來做不同的處理。