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