JVM系列-Java agent超詳細知識梳理
一、簡介
1 開篇
在梳理SkyWalking agent
的plugin
、elasticsearch
的plugin
、arthas
等技術的原理時,發現他們的底層原理很多是相同的。這類工具都用到了Java agent
、類加載、類隔離等技術,在此進行歸類梳理。
本篇將梳理Java agent
相關內容。在此先把這些技術整體的關係梳理如下:
二、 Java agent 使用場景
Java agent
技術結合 Java Intrumentation API
可以實現類修改、熱加載等功能,下面是 Java agent
技術的常見應用場景:
三、Java agent 示例
我們先用一個 Java agent
實現方法開始和結束時打印日誌的簡單例子來實踐一下,通過示例,可以很快對後面 Java agent
技術有初步的理解。
1 Java agent 實現方法開始和結束時打印日誌
1.1 開發 agent
創建 demo-javaagent 工程,目錄結構如下:
新建pom.xml
,引入javassist
用來修改目標類的字節碼,增加自定義代碼。通過maven-assembly-plugin
插件打包自定義的 agent jar。
```xml
<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
編寫 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 包,結果示例:
1.1.1 編寫驗證 agent 功能的測試類
創建 agent-example工程,目錄結構如下:
編寫測試 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的侷限性
在運行時,我們可以通過Instrumentation
的redefineClasses
方法進行類重定義,在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 agent
與Instrumentation
密不可分,二者也需要在一起使用。因為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
生成的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 時如果遇到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
// 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()
執行時的方法調用棧,
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 介紹
JVMTI
(JVM Tool Interface
)是 Java 虛擬機對外提供的 Native 編程接口,通過 JVMTI ,外部進程可以獲取到運行時JVM的諸多信息,比如線程、GC等。
JVMTI 是一套 Native 接口,在 Java SE 5 之前,要實現一個 Agent 只能通過編寫 Native 代碼來實現。從 Java SE 5 開始,可以使用 Java 的Instrumentation
接口(java.lang.instrument)來編寫 Agent。無論是通過 Native 的方式還是通過 Java Instrumentation 接口的方式來編寫 Agent,它們的工作都是藉助 JVMTI 來進行完成。
啟動方式
JVMTI
和Instumentation API
的作用很相似,都是一套 JVM 操作和監控的接口,且都需要通過agent來啟動:
Instumentation API
需要打包成 jar,並通過 Java agent 加載(對應啟動參數:-javaagent
)- JVMTI 需要打包成動態鏈接庫(隨操作系統,如.dll/.so文件),並通過 JVMTI agent 加載(對應啟動參數:
-agentlib/-agentpath
)
2 加載時機
啟動時(Agent_OnLoad)和運行時Attach(Agent_OnAttach)
3 功能
Instumentation API 可以支持 Java 語言實現 agent 功能,但是 JVMTI 功能比 Instumentation API 更強大,它支持:
- 獲取所有線程、查看線程狀態、線程調用棧、查看線程組、中斷線程、查看線程持有和等待的鎖、獲取線程的CPU時間、甚至將一個運行中的方法強制返回值……
- 獲取Class、Method、Field的各種信息,類的詳細信息、方法體的字節碼和行號、向Bootstrap/System Class Loader添加jar、修改System Property……
- 堆內存的遍歷和對象獲取、獲取局部變量的值、監測成員變量的值……
- 各種事件的callback函數,事件包括:類文件加載、異常產生與捕獲、線程啟動和結束、進入和退出臨界區、成員變量修改、gc開始和結束、方法調用進入和退出、臨界區競爭與等待、VM啟動與退出……
- 設置與取消斷點、監聽斷點進入事件、單步執行事件……
4 JVMTI 與 Java agent
Java agent 是基於 JVMTI 實現,核心部分是 ClassFileLoadHook
和TransFormClassFile
。
ClassFileLoadHook
是一個 JVMTI 事件,該事件是 Instrumentation agent 的一個核心事件,主要是在讀取字節碼文件回調時調用,內部調用了TransFormClassFile
的函數。
TransFormClassFile
的主要作用是調用java.lang.instrument.ClassFileTransformer
的tranform
方法,該方法由開發者實現,通過Instrumentation
的addTransformer
方法進行註冊。
在字節碼文件加載的時候,會觸發ClassFileLoadHook
事件,該事件調用TransFormClassFile
,通過經由Instrumentation
的 addTransformer
註冊的方法完成整體的字節碼修改。
對於已加載的類,需要調用retransformClass
函數,然後經由redefineClasses
函數,在讀取已加載的字節碼文件後,若該字節碼文件對應的類關注了ClassFileLoadHook
事件,則調用ClassFileLoadHook
事件。後續流程與類加載時字節碼替換一致。
七、Attach API
前文提到,Java agent 動態加載是通過 Attach API 實現。
1 Attach API 介紹
Attach機制是JVM提供一種JVM進程間通信的能力,能讓一個進程傳命令給另外一個進程,並讓它執行內部的一些操作。
日常很多工作都是通過 Attach API 實現的,示例:
- JDK 自帶的一些命令,如:jstack打印線程棧、jps列出Java進程、jmap做內存dump等功能
- Arthas、Greys、btrace 等監控診斷產品,通過 attach 目標 JVM 進程發送指定命令,可以實現方法調用等方面的監控。
2 Attach API 用法
由於是進程間通訊,那代表着使用Attach API的程序需要是一個獨立的Java程序,通過attach目標進程,與其進行通訊。下面的代碼表示了向進程pid為1234的JVM發起通訊,加載一個名為agent.jar的Java agent。
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() 方法。
執行的流程圖:
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會針對不同的傳入數據來做不同的處理。