Java JVMTI和Instrumention機制介紹

也可以看我的CSDN上的博客:
https://blog.csdn.net/u013332124/article/details/88367630

1、JVMTI 介紹

JVMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。

JVMTI可以用來開發(fā)并監(jiān)控虛擬機,可以查看JVM內(nèi)部的狀態(tài),并控制JVM應(yīng)用程序的執(zhí)行??蓪崿F(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啟動時指定加載運行該agent。

-agentlib:<agent-lib-name>=<options>

之后JVM啟動后該agent程序就會開始工作。

1.1 Agent的工作形式

agent啟動后是和JVM運行在同一個進程,大多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)試工具了。但是相對還是比較費時費力,所以才有了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機器,并且服務(wù)端和客戶端位于一臺機器上
  • server=y 表示當(dāng)前是調(diào)試服務(wù)端,=n表示當(dāng)前是調(diào)試客戶端
  • suspend=n 表示啟動時不中斷(如果啟動時中斷,一般用于調(diào)試啟動不了的問題)
  • address=8000 表示本地監(jiān)聽8000端口

2、Instrumention 機制

雖然java提供了JVMTI,但是對應(yīng)的agent需要用C/C++開發(fā),對java開發(fā)者而言并不是非常友好。因此在Java SE 5的新特性中加入了Instrumentation機制。有了 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)虛擬機層面的AOP。

redefineClasses 和 retransformClasses 的區(qū)別:

  1. transform是對類的byte流進行讀取轉(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傳入。我們可以拿到這個實例后進行各種操作
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類,就會進行類定義轉(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 - 要定義或重定義的類的保護域
     * 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類進行測試。

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();
    }
}

之后運行時指定加上JVM參數(shù) -javaagent:/toPath/agent-jar-with-dependencies.jar 就會發(fā)現(xiàn)Test已經(jīng)被轉(zhuǎn)換了。

2.3 如何在運行時加載agent

上面開發(fā)的agent需要啟動就必須在jvm啟動時設(shè)置參數(shù),但很多時候我們想要在程序運行時中途插入一個agent運行。在Java 6的新特性中,就可以通過Attach的方式去加載一個agent了。

關(guān)于Attach的機制原理可以看我的這篇博客:

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);
        //由于是在運行中才加入了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)進程并加載這個agent

public class AgentMainStarter {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
            AgentInitializationException {
                //這個pid填寫具體要attach的目標(biāo)進程
        VirtualMachine attach = VirtualMachine.attach("pid");
        attach.loadAgent("/toPath/agent-jar-with-dependencies.jar");
        attach.detach();
        System.out.println("over");
    }
}

之后修改一下Test類,讓他不斷運行下去

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);
        }
    }
}

運行Test一段時間后,再運行AgentMainStarter類,會發(fā)現(xiàn)輸出變成了最早編譯的那個/tmp/Test.class下面的"A"了。說明我們的agent進程已經(jīng)在目標(biāo)JVM成功運行。

3、參考資料

Java Attach機制簡介

基于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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容