Java JVMTI和Instrumention機(jī)制介紹

也可以看我的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個模塊組成:

  1. JVMTI落塑,即底層的相關(guān)調(diào)試接口調(diào)用。sun公司提供了一個 jdwp.dll( jdwp.so)動態(tài)鏈接庫罐韩,就是我們上面說的agent實現(xiàn)芜赌。
  2. JDWP(Java Debug Wire Protocol),定義了agent和調(diào)試客戶端之間的通訊交互協(xié)議。
  3. 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ū)別:

  1. transform是對類的byte流進(jìn)行讀取轉(zhuǎn)換的過程晴及,需要先獲取類的byte流然后做修改都办。而redefineClasses更簡單粗暴一些,它需要直接給出新的類byte流虑稼,然后替換舊的琳钉。
  2. 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 Attach機(jī)制簡介

基于Java Instrument的Agent實現(xiàn)

IBM: Instrumentation 新功能

Instrumentation 中redefineClasses 和 retransformClasses 的區(qū)別

JVMTI開發(fā)文檔

JVMTI oracle 官方文檔

JVMTI和JDPA介紹

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煞聪,一起剝皮案震驚了整個濱河市斗躏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昔脯,老刑警劉巖啄糙,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異云稚,居然都是意外死亡隧饼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門静陈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燕雁,“玉大人诞丽,你說我怎么就攤上這事」崭瘢” “怎么了僧免?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捏浊。 經(jīng)常有香客問我懂衩,道長,這世上最難降的妖魔是什么呛伴? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任勃痴,我火速辦了婚禮,結(jié)果婚禮上热康,老公的妹妹穿的比我還像新娘沛申。我一直安慰自己,他們只是感情好姐军,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布铁材。 她就那樣靜靜地躺著,像睡著了一般奕锌。 火紅的嫁衣襯著肌膚如雪著觉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天惊暴,我揣著相機(jī)與錄音饼丘,去河邊找鬼。 笑死辽话,一個胖子當(dāng)著我的面吹牛肄鸽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播油啤,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼典徘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了益咬?” 一聲冷哼從身側(cè)響起逮诲,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎幽告,沒想到半個月后梅鹦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡冗锁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年帘瞭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒿讥。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蝶念,死狀恐怖抛腕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媒殉,我是刑警寧澤担敌,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站廷蓉,受9級特大地震影響全封,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桃犬,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一刹悴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攒暇,春花似錦土匀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至田度,卻和暖如春妒御,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镇饺。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工乎莉, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奸笤。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓梦鉴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親揭保。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容