也可以看我的CSDN上的博客:
https://blog.csdn.net/u013332124/article/details/88367630
1、JVMTI 介紹
JVMTI(JVM Tool Interface)是 Java 虛擬機(jī)所提供的 native 編程接口沽瘦,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本忿偷。
JVMTI可以用來開發(fā)并監(jiān)控虛擬機(jī)寇壳,可以查看JVM內(nèi)部的狀態(tài)添坊,并控制JVM應(yīng)用程序的執(zhí)行太伊」臀可實現(xiàn)的功能包括但不限于:調(diào)試、監(jiān)控僚焦、線程分析锰提、覆蓋率分析工具等。
另外芳悲,需要注意的是立肘,并非所有的JVM實現(xiàn)都支持JVMTI。
JVMTI只是一套接口名扛,我們要開發(fā)JVM工具就需要寫一個Agent程序來使用這些接口谅年。Agent程序其實就是一個C/C++語言編寫的動態(tài)鏈接庫。這里不詳細(xì)介紹如何開發(fā)一個JVMTI的agent程序罢洲。感興趣的可以點擊文章末尾的鏈接查看踢故。
我們通過JVMTI開發(fā)好agent程序后,把程序編譯成動態(tài)鏈接庫惹苗,之后可以在jvm啟動時指定加載運(yùn)行該agent殿较。
-agentlib:<agent-lib-name>=<options>
之后JVM啟動后該agent程序就會開始工作。
1.1 Agent的工作形式
agent啟動后是和JVM運(yùn)行在同一個進(jìn)程桩蓉,大多agent的工作形式是作為服務(wù)端接收來自客戶端的請求淋纲,然后根據(jù)請求命令調(diào)用JVMTI的相關(guān)接口再返回結(jié)果。
很多java監(jiān)控院究、診斷工具都是基于這種形式來工作的洽瞬。如果arthas、jinfo业汰、brace等伙窃。
另外,我們熟知的java調(diào)試也是其實也是基于這種工作原理样漆。
1.2 JDPA 相關(guān)介紹
無論我們在開發(fā)調(diào)試時为障,都會用到調(diào)試工具。其實我們用的所有調(diào)試工具其底層都是基于JVMTI的調(diào)用。JVMTI本身就提供了關(guān)于調(diào)試程序的一系列接口鳍怨,我們只需要編寫agent就可以開發(fā)一套調(diào)試工具了呻右。
雖然對應(yīng)的接口已經(jīng)有了,但是要基于這些接口開發(fā)一套完整的調(diào)試工具還是有一定工作量的鞋喇。為了避免重復(fù)造輪子声滥,sun公司定義了一套完整獨立的調(diào)試體系,也就是JDPA侦香。
JDPA由3個模塊組成:
- JVMTI落塑,即底層的相關(guān)調(diào)試接口調(diào)用。sun公司提供了一個 jdwp.dll( jdwp.so)動態(tài)鏈接庫罐韩,就是我們上面說的agent實現(xiàn)芜赌。
- JDWP(Java Debug Wire Protocol),定義了agent和調(diào)試客戶端之間的通訊交互協(xié)議。
- JDI(Java Debug Interface)伴逸,是由Java語言實現(xiàn)的。有了這套接口膘壶,我們就可以直接使用java開發(fā)一套自己的調(diào)試工具错蝴。
[圖片上傳失敗...(image-3bb125-1552119475529)]
其實有了jdwp Agent以及知道了交互的消息協(xié)議格式,我們就可以基于這些開發(fā)一套調(diào)試工具了颓芭。但是相對還是比較費(fèi)時費(fèi)力顷锰,所以才有了JDI的誕生,JDI是一套JAVA API亡问。這樣對于不熟悉C/C++的java程序員也能開發(fā)自己的調(diào)試工具了官紫。
另外,JDI 不僅能幫助開發(fā)人員格式化 JDWP 數(shù)據(jù)州藕,而且還能為 JDWP 數(shù)據(jù)傳輸提供隊列束世、緩存等優(yōu)化服務(wù)
再回頭看一下啟動JVM debug時需要帶上的參數(shù):
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar
jdwp.dll作為一個jvm內(nèi)置的agent,不需要上文說的-agentlib來啟動agent床玻。這里通過-Xrunjdwp
來啟動該agent毁涉。后面還指定了一些參數(shù):
- transport=dt_socket,表示用監(jiān)聽socket端口的方式來建立連接锈死,這里也可以選擇dt_shmem共享內(nèi)存方式贫堰,但限于windows機(jī)器,并且服務(wù)端和客戶端位于一臺機(jī)器上
- server=y 表示當(dāng)前是調(diào)試服務(wù)端待牵,=n表示當(dāng)前是調(diào)試客戶端
- suspend=n 表示啟動時不中斷(如果啟動時中斷其屏,一般用于調(diào)試啟動不了的問題)
- address=8000 表示本地監(jiān)聽8000端口
2、Instrumention 機(jī)制
雖然java提供了JVMTI缨该,但是對應(yīng)的agent需要用C/C++開發(fā)偎行,對java開發(fā)者而言并不是非常友好。因此在Java SE 5的新特性中加入了Instrumentation機(jī)制。有了 Instrumentation睦优,開發(fā)者可以構(gòu)建一個基于Java編寫的Agent來監(jiān)控或者操作JVM了渗常,比如替換或者修改某些類的定義等。
2.1 Instrumention支持的功能
Instrumention支持的功能都在java.lang.instrument.Instrumentation
接口中體現(xiàn):
public interface Instrumentation {
//添加一個ClassFileTransformer
//之后類加載時都會經(jīng)過這個ClassFileTransformer轉(zhuǎn)換
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
//將一些已經(jīng)加載過的類重新拿出來經(jīng)過注冊好的ClassFileTransformer轉(zhuǎn)換
//retransformation可以修改方法體汗盘,但是不能變更方法簽名皱碘、增加和刪除方法/類的成員屬性
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
//重新定義某個類
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
我們通過addTransformer方法注冊了一個ClassFileTransformer,后面類加載的時候都會經(jīng)過這個Transformer處理隐孽。對于已加載過的類癌椿,可以調(diào)用retransformClasses來重新觸發(fā)這個Transformer的轉(zhuǎn)換。
ClassFileTransformer可以判斷是否需要修改類定義并根據(jù)自己的代碼規(guī)則修改類定義然后返回給JVM菱阵。利用這個Transformer類踢俄,我們可以很好的實現(xiàn)虛擬機(jī)層面的AOP。
redefineClasses 和 retransformClasses 的區(qū)別:
- transform是對類的byte流進(jìn)行讀取轉(zhuǎn)換的過程晴及,需要先獲取類的byte流然后做修改都办。而redefineClasses更簡單粗暴一些,它需要直接給出新的類byte流虑稼,然后替換舊的琳钉。
- transform可以添加很多個,retransformClasses 可以讓指定的類重新經(jīng)過這些transform做轉(zhuǎn)換蛛倦。
2.2 基于Instrumention開發(fā)一個Agent
利用java.lang.instrument包下面的相關(guān)類歌懒,我們可以開發(fā)一個自己的Agent程序。
2.2.1 編寫premain函數(shù)
編寫一個java類溯壶,不用繼承或者實現(xiàn)任何類及皂,直接實現(xiàn)下面兩個方法中的任一方法:
//agentArgs是一個字符串,會隨著jvm啟動設(shè)置的參數(shù)得到
//inst就是我們需要的Instrumention實例了且改,由JVM傳入验烧。我們可以拿到這個實例后進(jìn)行各種操作
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
其中,[1] 的優(yōu)先級比 [2] 高钾虐,將會被優(yōu)先執(zhí)行噪窘,[1] 和 [2] 同時存在時,[2] 被忽略效扫。
編寫一個PreMain:
public class PreMain {
public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
UnmodifiableClassException {
inst.addTransformer(new MyTransform());
}
}
MyTransform是我們自己定義的一個ClassFileTransformer實現(xiàn)類倔监,這個類遇到com/yjb/Test
類,就會進(jìn)行類定義轉(zhuǎn)換菌仁。
public class MyTransform implements ClassFileTransformer {
public static final String classNumberReturns2 = "/tmp/Test.class";
public static byte[] getBytesFromFile(String fileName) {
try {
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
/**
* 參數(shù):
* loader - 定義要轉(zhuǎn)換的類加載器浩习;如果是引導(dǎo)加載器,則為 null
* className - 完全限定類內(nèi)部形式的類名稱和 The Java Virtual Machine Specification 中定義的接口名稱济丘。例如谱秽,"java/util/List"洽蛀。
* classBeingRedefined - 如果是被重定義或重轉(zhuǎn)換觸發(fā),則為重定義或重轉(zhuǎn)換的類疟赊;如果是類加載郊供,則為 null
* protectionDomain - 要定義或重定義的類的保護(hù)域
* classfileBuffer - 類文件格式的輸入字節(jié)緩沖區(qū)(不得修改)
* 返回:
* 一個格式良好的類文件緩沖區(qū)(轉(zhuǎn)換的結(jié)果),如果未執(zhí)行轉(zhuǎn)換,則返回 null近哟。
* 拋出:
* IllegalClassFormatException - 如果輸入不表示一個格式良好的類文件
*/
public byte[] transform(ClassLoader l, String className, Class<?> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
System.out.println("transform class-------" + className);
if (!className.equals("com/yjb/Test")) {
return null;
}
return getBytesFromFile(targetClassPath);
}
}
2.2.2 打成jar包
之后我們把上面兩個類打成一個jar包驮审,并在其中的META-INF/MAINIFEST.MF屬性當(dāng)中加入” Premain-Class”來指定成上面的PreMain類。
我們可以用maven插件來做到自動打包并寫MAINIFEST.MF:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>com.yjb.PreMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Specification-Title>${project.name}</Specification-Title>
<Specification-Version>${project.version}</Specification-Version>
<Implementation-Title>${project.name}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
2.2.3 編寫測試類
上面的agent會轉(zhuǎn)換com/yjb/Test
類吉执,我們就編寫一個Test類進(jìn)行測試疯淫。
public class Test {
public void print() {
System.out.println("A");
}
}
先編譯這個類,然后把Test.class 放到 /tmp 下戳玫。
之后再修改這個類:
public class Test {
public void print() {
System.out.println("B");
}
public static void main(String[] args) throws InterruptedException {
new Test().print();
}
}
之后運(yùn)行時指定加上JVM參數(shù) -javaagent:/toPath/agent-jar-with-dependencies.jar
就會發(fā)現(xiàn)Test已經(jīng)被轉(zhuǎn)換了隅津。
2.3 如何在運(yùn)行時加載agent
上面開發(fā)的agent需要啟動就必須在jvm啟動時設(shè)置參數(shù)畴蒲,但很多時候我們想要在程序運(yùn)行時中途插入一個agent運(yùn)行西疤。在Java 6的新特性中蜒简,就可以通過Attach的方式去加載一個agent了。
關(guān)于Attach的機(jī)制原理可以看我的這篇博客:
https://blog.csdn.net/u013332124/article/details/88362317
使用這種方式加載的agent啟動類需要實現(xiàn)這兩種方法中的一種:
public static void agentmain (String agentArgs, Instrumentation inst); [1]
public static void agentmain (String agentArgs);[2]
和premain一樣府阀,[1] 比 [2] 的優(yōu)先級高类浪。
之后要在META-INF/MAINIFEST.MF屬性當(dāng)中加入” AgentMain-Class”來指定目標(biāo)啟動類。
我們可以在上面的agent項目中加入一個AgentMain類
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
UnmodifiableClassException, InterruptedException {
//這里的Transform還是使用上面定義的那個
inst.addTransformer(new MyTransform(), true);
//由于是在運(yùn)行中才加入了Transform肌似,因此需要重新retransformClasses一下
Class<?> aClass = Class.forName("com.yjb.Test");
inst.retransformClasses(aClass);
System.out.println("Agent Main Done");
}
}
還是把項目打包成agent-jar-with-dependencies.jar
。
之后再編寫一個類去attach目標(biāo)進(jìn)程并加載這個agent
public class AgentMainStarter {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
AgentInitializationException {
//這個pid填寫具體要attach的目標(biāo)進(jìn)程
VirtualMachine attach = VirtualMachine.attach("pid");
attach.loadAgent("/toPath/agent-jar-with-dependencies.jar");
attach.detach();
System.out.println("over");
}
}
之后修改一下Test類诉瓦,讓他不斷運(yùn)行下去
public class Test {
private void print() {
System.out.println("1111");
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
while (true) {
test.print();
Thread.sleep(1000L);
}
}
}
運(yùn)行Test一段時間后川队,再運(yùn)行AgentMainStarter類,會發(fā)現(xiàn)輸出變成了最早編譯的那個/tmp/Test.class下面的"A"了睬澡。說明我們的agent進(jìn)程已經(jīng)在目標(biāo)JVM成功運(yùn)行固额。
3、參考資料
基于Java Instrument的Agent實現(xiàn)
Instrumentation 中redefineClasses 和 retransformClasses 的區(qū)別