Java JVMTI和Instrumention機制介紹

也能夠看個人CSDN上的博客:
https://blog.csdn.net/u013332124/article/details/88367630html

一、JVMTI 介紹

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

JVMTI能夠用來開發並監控虛擬機,能夠查看JVM內部的狀態,並控制JVM應用程序的執行。可實現的功能包括但不限於:調試、監控、線程分析、覆蓋率分析工具等。程序員

另外,須要注意的是,並不是全部的JVM實現都支持JVMTI。apache

JVMTI只是一套接口,咱們要開發JVM工具就須要寫一個Agent程序來使用這些接口。Agent程序其實就是一個C/C++語言編寫的動態連接庫。這裏不詳細介紹如何開發一個JVMTI的agent程序。感興趣的能夠點擊文章末尾的連接查看。編程

咱們經過JVMTI開發好agent程序後,把程序編譯成動態連接庫,以後能夠在jvm啓動時指定加載運行該agent。windows

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

以後JVM啓動後該agent程序就會開始工做。緩存

1.1 Agent的工做形式

agent啓動後是和JVM運行在同一個進程,大多agent的工做形式是做爲服務端接收來自客戶端的請求,而後根據請求命令調用JVMTI的相關接口再返回結果。oracle

不少java監控、診斷工具都是基於這種形式來工做的。若是arthas、jinfo、brace等。app

另外,咱們熟知的java調試也是其實也是基於這種工做原理。jvm

1.2 JDPA 相關介紹

不管咱們在開發調試時,都會用到調試工具。其實咱們用的全部調試工具其底層都是基於JVMTI的調用。JVMTI自己就提供了關於調試程序的一系列接口,咱們只須要編寫agent就能夠開發一套調試工具了。

雖然對應的接口已經有了,可是要基於這些接口開發一套完整的調試工具仍是有必定工做量的。爲了不重複造輪子,sun公司定義了一套完整獨立的調試體系,也就是JDPA。

JDPA由3個模塊組成:

  1. JVMTI,即底層的相關調試接口調用。sun公司提供了一個 jdwp.dll( jdwp.so)動態連接庫,就是咱們上面說的agent實現。
  2. JDWP(Java Debug Wire Protocol),定義了agent和調試客戶端之間的通信交互協議。
  3. JDI(Java Debug Interface),是由Java語言實現的。有了這套接口,咱們就能夠直接使用java開發一套本身的調試工具。

[圖片上傳失敗...(image-3bb125-1552119475529)]

其實有了jdwp Agent以及知道了交互的消息協議格式,咱們就能夠基於這些開發一套調試工具了。可是相對仍是比較費時費力,因此纔有了JDI的誕生,JDI是一套JAVA API。這樣對於不熟悉C/C++的java程序員也能開發本身的調試工具了。

另外,JDI 不只能幫助開發人員格式化 JDWP 數據,並且還能爲 JDWP 數據傳輸提供隊列、緩存等優化服務

再回頭看一下啓動JVM debug時須要帶上的參數:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar

jdwp.dll做爲一個jvm內置的agent,不須要上文說的-agentlib來啓動agent。這裏經過-Xrunjdwp來啓動該agent。後面還指定了一些參數:

  • transport=dt_socket,表示用監聽socket端口的方式來創建鏈接,這裏也能夠選擇dt_shmem共享內存方式,但限於windows機器,而且服務端和客戶端位於一臺機器上
  • server=y 表示當前是調試服務端,=n表示當前是調試客戶端
  • suspend=n 表示啓動時不中斷(若是啓動時中斷,通常用於調試啓動不了的問題)
  • address=8000 表示本地監聽8000端口

二、Instrumention 機制

雖然java提供了JVMTI,可是對應的agent須要用C/C++開發,對java開發者而言並非很是友好。所以在Java SE 5的新特性中加入了Instrumentation機制。有了 Instrumentation,開發者能夠構建一個基於Java編寫的Agent來監控或者操做JVM了,好比替換或者修改某些類的定義等。

2.1 Instrumention支持的功能

Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中體現:

public interface Instrumentation {
    //添加一個ClassFileTransformer
    //以後類加載時都會通過這個ClassFileTransformer轉換
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    void addTransformer(ClassFileTransformer transformer);
    //移除ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();
    //將一些已經加載過的類從新拿出來通過註冊好的ClassFileTransformer轉換
    //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,後面類加載的時候都會通過這個Transformer處理。對於已加載過的類,能夠調用retransformClasses來從新觸發這個Transformer的轉換

ClassFileTransformer能夠判斷是否須要修改類定義並根據本身的代碼規則修改類定義而後返回給JVM。利用這個Transformer類,咱們能夠很好的實現虛擬機層面的AOP。

redefineClasses 和 retransformClasses 的區別:

  1. transform是對類的byte流進行讀取轉換的過程,須要先獲取類的byte流而後作修改。而redefineClasses更簡單粗暴一些,它須要直接給出新的類byte流,而後替換舊的
  2. transform能夠添加不少個,retransformClasses 可讓指定的類從新通過這些transform作轉換。

2.2 基於Instrumention開發一個Agent

利用java.lang.instrument包下面的相關類,咱們能夠開發一個本身的Agent程序。

2.2.1 編寫premain函數

編寫一個java類,不用繼承或者實現任何類,直接實現下面兩個方法中的任一方法:

//agentArgs是一個字符串,會隨着jvm啓動設置的參數獲得
//inst就是咱們須要的Instrumention實例了,由JVM傳入。咱們能夠拿到這個實例後進行各類操做
public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]

其中,[1] 的優先級比 [2] 高,將會被優先執行,[1] 和 [2] 同時存在時,[2] 被忽略。

編寫一個PreMain:

public class PreMain {

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
            UnmodifiableClassException {
        inst.addTransformer(new MyTransform());
    }
}

MyTransform是咱們本身定義的一個ClassFileTransformer實現類,這個類遇到com/yjb/Test類,就會進行類定義轉換。

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

    /**
     * 參數:
     * loader - 定義要轉換的類加載器;若是是引導加載器,則爲 null
     * className - 徹底限定類內部形式的類名稱和 The Java Virtual Machine Specification 中定義的接口名稱。例如,"java/util/List"。
     * classBeingRedefined - 若是是被重定義或重轉換觸發,則爲重定義或重轉換的類;若是是類加載,則爲 null
     * protectionDomain - 要定義或重定義的類的保護域
     * classfileBuffer - 類文件格式的輸入字節緩衝區(不得修改)
     * 返回:
     * 一個格式良好的類文件緩衝區(轉換的結果),若是未執行轉換,則返回 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屬性當中加入」 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會轉換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參數 -javaagent:/toPath/agent-jar-with-dependencies.jar 就會發現Test已經被轉換了

2.3 如何在運行時加載agent

上面開發的agent須要啓動就必須在jvm啓動時設置參數,但不少時候咱們想要在程序運行時中途插入一個agent運行。在Java 6的新特性中,就能夠經過Attach的方式去加載一個agent了。

關於Attach的機制原理能夠看個人這篇博客:

https://blog.csdn.net/u013332124/article/details/88362317

使用這種方式加載的agent啓動類須要實現這兩種方法中的一種:

public static void agentmain (String agentArgs, Instrumentation inst); [1] 
public static void agentmain (String agentArgs);[2]

和premain同樣,[1] 比 [2] 的優先級高。

以後要在META-INF/MAINIFEST.MF屬性當中加入」 AgentMain-Class」來指定目標啓動類

咱們能夠在上面的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目標進程並加載這個agent

public class AgentMainStarter {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
            AgentInitializationException {
                //這個pid填寫具體要attach的目標進程
        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類,會發現輸出變成了最先編譯的那個/tmp/Test.class下面的"A"了。說明咱們的agent進程已經在目標JVM成功運行。

三、參考資料

Java Attach機制簡介

基於Java Instrument的Agent實現

IBM: Instrumentation 新功能

Instrumentation 中redefineClasses 和 retransformClasses 的區別

JVMTI開發文檔

JVMTI oracle 官方文檔

JVMTI和JDPA介紹

相關文章
相關標籤/搜索