1. 程式人生 > 程式設計 >java agent使用全解析

java agent使用全解析

今天打算寫一下 Java agent,一開始我對它的概念也比較陌生,後來在別人口中聽到 位元組碼插樁,bTrace,Arthas後面才逐漸瞭解到Java還提供了這麼個工具。

JVM啟動前靜態Instrument

Java agent 是什麼?

Java agent是java命令的一個引數。引數 javaagent 可以用於指定一個 jar 包,並且對該 java 包有2個要求:

  1. 這個 jar 包的 MANIFEST.MF 檔案必須指定 Premain-Class 項。
  2. Premain-Class 指定的那個類必須實現 premain() 方法。

premain 方法,從字面上理解,就是執行在 main 函式之前的的類。當Java 虛擬機器啟動時,在執行 main 函式之前,JVM 會先執行-javaagent

所指定 jar 包內 Premain-Class 這個類的 premain 方法 。

在命令列輸入 java可以看到相應的引數,其中有 和 java agent相關的:

-agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>,例如 -agentlib:hprof
另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
按完整路徑名載入本機代理庫
-javaagent:<jarpath>[=<選項>]

載入 Java 程式語言代理,請參閱 java.lang.instrument

在上面-javaagent引數中提到了參閱java.lang.instrument,這是在rt.jar 中定義的一個包,該路徑下有兩個重要的類:

java agent使用全解析

該包提供了一些工具幫助開發人員在 Java 程式執行時,動態修改系統中的 Class 型別。其中,使用該軟體包的一個關鍵元件就是 Javaagent。從名字上看,似乎是個 Java 代理之類的,而實際上,他的功能更像是一個Class 型別的轉換器,他可以在執行時接受重新外部請求,對Class型別進行修改。

從本質上講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必須要有premain()方法,並且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:

public static void premain(String agentArgs,Instrumentation inst)
  
public static void premain(String agentArgs)

JVM 會優先載入 帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中:

java agent使用全解析

Instrumentation 類 定義如下:

public interface Instrumentation {
  
  //增加一個Class 檔案的轉換器,轉換器用於改變 Class 二進位制流的資料,引數 canRetransform 設定是否允許重新轉換。
  void addTransformer(ClassFileTransformer transformer,boolean canRetransform);

  //在類載入之前,重新定義 Class 檔案,ClassDefinition 表示對一個類新的定義,如果在類載入之後,需要使用 retransformClasses 方法重新定義。addTransformer方法配置之後,後續的類載入都會被Transformer攔截。對於已經載入過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截。類載入的位元組碼被修改後,除非再次被retransform,否則不會恢復。
  void addTransformer(ClassFileTransformer transformer);

  //刪除一個類轉換器
  boolean removeTransformer(ClassFileTransformer transformer);

  boolean isRetransformClassesSupported();

  //在類載入之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。
  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);
}

最為重要的是上面註釋的幾個方法,下面我們會用到。

如何使用javaagent?

使用 javaagent 需要幾個步驟:

  1. 定義一個 MANIFEST.MF 檔案,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 建立一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由使用者自己確定。
  3. 將 premain 的類和 MANIFEST.MF 檔案打成 jar 包。
  4. 使用引數 -javaagent: jar包路徑 啟動要代理的方法。

在執行以上步驟後,JVM 會先執行 premain 方法,大部分類載入都會通過該方法,注意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而使用者類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,那麼就可以去做重寫類這樣的操作,結合第三方的位元組碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。

通過上面的步驟我們用程式碼實現來實現。實現 javaagent 你需要搭建兩個工程,一個工程是用來承載 javaagent類,單獨的打成jar包;一個工程是javaagent需要去代理的類。即javaagent會在這個工程中的main方法啟動之前去做一些事情。

1.首先來實現javaagent工程。

工程目錄結構如下:

-java-agent
----src
--------main
--------|------java
--------|----------com.rickiyang.learn
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF

第一步是需要建立一個類,包含premain 方法:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author: rickiyang
 * @date: 2019/8/12
 * @description:
 */
public class PreMainTraceAgent {

  public static void premain(String agentArgs,Instrumentation inst) {
    System.out.println("agentArgs : " + agentArgs);
    inst.addTransformer(new DefineTransformer(),true);
  }

  static class DefineTransformer implements ClassFileTransformer{

    @Override
    public byte[] transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {
      System.out.println("premain load Class:" + className);
      return classfileBuffer;
    }
  }
}

上面就是我實現的一個類,實現了帶Instrumentation引數的premain()方法。呼叫addTransformer()方法對啟動時所有的類進行攔截。

然後在 resources 目錄下新建目錄:META-INF,在該目錄下新建檔案:MANIFREST.MF:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent

注意到第5行有空行。

說一下MANIFREST.MF檔案的作用,這裡如果你不去手動指定的話,直接 打包,預設會在打包的檔案中生成一個MANIFREST.MF檔案:

Manifest-Version: 1.0
Implementation-Title: test-agent
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: yangyue
Implementation-Vendor-Id: com.rickiyang.learn
Spring-Boot-Version: 2.0.9.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rickiyang.learn.LearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_151
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/test-agent

這是預設的檔案,包含當前的一些版本資訊,當前工程的啟動類,它還有別的引數允許你做更多的事情,可以用上的有:

  • Premain-Class :包含 premain 方法的類(類的全路徑名)
  • Agent-Class :包含 agentmain 方法的類(類的全路徑名)
  • Boot-Class-Path :設定引導類載入器搜尋的路徑列表。查詢類的特定於平臺的機制失敗後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件語法。如果該路徑以斜槓字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之後某一時刻啟動的,則忽略不表示 JAR 檔案的路徑。(可選)
  • Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)
  • Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)
  • Can-Set-Native-Method-Prefix: true表示能設定此代理所需的本機方法字首,預設值為 false(可選)

即在該檔案中主要定義了程式執行相關的配置資訊,程式執行前會先檢測該檔案中的配置項。

一個java程式中-javaagent引數的個數是沒有限制的,所以可以新增任意多個javaagent。所有的java agent會按照你定義的順序執行,例如:

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

程式執行的順序將會是:

MyAgent1.premain -> MyAgent2.premain -> MyProgram.main

說回上面的 javaagent工程,接下來將該工程打成jar包,我在打包的時候發現打完包之後的 MANIFREST.MF檔案被預設配置替換掉了。所以我是手動將上面我的配置檔案替換到jar包中的檔案,這裡你需要注意。

另外的再說一種不去手動寫MANIFREST.MF檔案的方式,使用maven外掛:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.1.0</version>
  <configuration>
    <archive>
      <!--自動新增META-INF/MANIFEST.MF -->
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class>
        <Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class>
        <Can-Redefine-Classes>true</Can-Redefine-Classes>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

用這種外掛的方式也可以自動生成該檔案。

agent程式碼就寫完了,下面再重新開一個工程,你只需要寫一個帶 main 方法的類即可:

public class TestMain {

  public static void main(String[] args) {
    System.out.println("main start");
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("main end");
  }
}

很簡單,然後需要做的就是將上面的 代理類 和 這個測試類關聯起來。有兩種方式:

如果你用的是idea,那麼你可以點選選單: run-debug configuration,然後將你的代理類包 指定在 啟動引數中即可:

java agent使用全解析

另一種方式是不用 編譯器,採用命令列的方法。與上面大致相同,將 上面的測試類編譯成 class檔案,然後 執行該類即可:

 #將該類編譯成class檔案
 > javac TestMain.java
 
 #指定agent程式並執行該類
 > java -javaagent:c:/alg.jar TestMain

使用上面兩種方式都可以執行,輸出結果如下:

D:\soft\jdk1.8\bin\java.exe -javaagent:c:/alg.jar "-javaagent:D:\soft\IntelliJ IDEA 2019.1.1\lib\idea_rt.jar=54274:D:\soft\IntelliJ IDEA 2019.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\soft\jdk1.8\jre\lib\charsets.jar;D:\soft\jdk1.8\jre\lib\deploy.jar;D:\soft\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\soft\jdk1.8\jre\lib\ext\cldrdata.jar;D:\soft\jdk1.8\jre\lib\ext\dnsns.jar;D:\soft\jdk1.8\jre\lib\ext\jaccess.jar;D:\soft\jdk1.8\jre\lib\ext\jfxrt.jar;D:\soft\jdk1.8\jre\lib\ext\localedata.jar;D:\soft\jdk1.8\jre\lib\ext\nashorn.jar;D:\soft\jdk1.8\jre\lib\ext\sunec.jar;D:\soft\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\soft\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\soft\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\soft\jdk1.8\jre\lib\ext\zipfs.jar;D:\soft\jdk1.8\jre\lib\javaws.jar;D:\soft\jdk1.8\jre\lib\jce.jar;D:\soft\jdk1.8\jre\lib\jfr.jar;D:\soft\jdk1.8\jre\lib\jfxswt.jar;D:\soft\jdk1.8\jre\lib\jsse.jar;D:\soft\jdk1.8\jre\lib\management-agent.jar;D:\soft\jdk1.8\jre\lib\plugin.jar;D:\soft\jdk1.8\jre\lib\resources.jar;D:\soft\jdk1.8\jre\lib\rt.jar;D:\workspace\demo1\target\classes;E:\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.1.1.RELEASE\spring-
...
...
...
1.8.11.jar;E:\.m2\repository\com\google\guava\guava\20.0\guava-20.0.jar;E:\.m2\repository\org\apache\commons\commons-lang3\3.7\commons-lang3-3.7.jar;E:\.m2\repository\com\alibaba\fastjson\1.2.54\fastjson-1.2.54.jar;E:\.m2\repository\org\springframework\boot\spring-boot\2.1.0.RELEASE\spring-boot-2.1.0.RELEASE.jar;E:\.m2\repository\org\springframework\spring-context\5.1.3.RELEASE\spring-context-5.1.3.RELEASE.jar com.springboot.example.demo.service.TestMain
agentArgs : null
premain load Class :java/util/concurrent/ConcurrentHashMap$ForwardingNode
premain load Class :sun/nio/cs/ThreadLocalCoders
premain load Class :sun/nio/cs/ThreadLocalCoders$1
premain load Class :sun/nio/cs/ThreadLocalCoders$Cache
premain load Class :sun/nio/cs/ThreadLocalCoders$2
premain load Class :java/util/jar/Attributes
premain load Class :java/util/jar/Manifest$FastInputStream
...
...
...
premain load Class :java/lang/Class$MethodArray
premain load Class :java/lang/Void
main start
premain load Class :sun/misc/VMSupport
premain load Class :java/util/Hashtable$KeySet
premain load Class :sun/nio/cs/ISO_8859_1$Encoder
premain load Class :sun/nio/cs/Surrogate$Parser
premain load Class :sun/nio/cs/Surrogate
...
...
...
premain load Class :sun/util/locale/provider/LocaleResources$ResourceReference
main end
premain load Class :java/lang/Shutdown
premain load Class :java/lang/Shutdown$Lock

Process finished with exit code 0

上面的輸出結果我們能夠發現:

  • 執行main方法之前會載入所有的類,包括系統類和自定義類;
  • 在ClassFileTransformer中會去攔截系統類和自己實現的類物件;
  • 如果你有對某些類物件進行改寫,那麼在攔截的時候抓住該類使用位元組碼編譯工具即可實現。

下面是使用javassist來動態將某個方法替換掉:

package com.rickiyang.learn;

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class MyClassTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(final ClassLoader loader,final String className,final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain,final byte[] classfileBuffer) {
    // 操作Date類
    if ("java/util/Date".equals(className)) {
      try {
        // 從ClassPool獲得CtClass物件
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass clazz = classPool.get("java.util.Date");
        CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
        //這裡對 java.util.Date.convertToAbbr() 方法進行了改寫,在 return之前增加了一個 列印操作
        String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
            "sb.append(name.charAt(1)).append(name.charAt(2));" +
            "System.out.println(\"sb.toString()\");" +
            "return sb;}";
        convertToAbbr.setBody(methodBody);

        // 返回位元組碼,並且detachCtClass物件
        byte[] byteCode = clazz.toBytecode();
        //detach的意思是將記憶體中曾經被javassist載入過的Date物件移除,如果下次有需要在記憶體中找不到會重新走javassist載入
        clazz.detach();
        return byteCode;
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }
    // 如果返回null則位元組碼不會被修改
    return null;
  }
}

JVM啟動後動態Instrument

上面介紹的Instrumentation是在 JDK 1.5中提供的,開發者只能在main載入之前新增手腳,在 Java SE 6 的 Instrumentation 當中,提供了一個新的代理操作方法:agentmain,可以在 main 函式開始執行之後再執行。

跟premain函式一樣, 開發者可以編寫一個含有agentmain函式的 Java 類:

//採用attach機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被載入完成,這個時候需要藉助Instrumentation#retransformClasses(Class<?>... classes)讓對應的類可以重新轉換,從而啟用重新轉換的類執行ClassFileTransformer列表中的回撥
public static void agentmain (String agentArgs,Instrumentation inst)

public static void agentmain (String agentArgs)

同樣,agentmain 方法中帶Instrumentation引數的方法也比不帶優先順序更高。開發者必須在 manifest 檔案裡面設定“Agent-Class”來指定包含 agentmain 函式的類。

在Java6 以後實現啟動後加載的新實現是Attach api。Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裡面:

java agent使用全解析

  1. VirtualMachine 字面意義表示一個Java 虛擬機器,也就是程式需要監控的目標虛擬機器,提供了獲取系統資訊(比如獲取記憶體dump、執行緒dump,類資訊統計(比如已載入的類以及例項個數等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實現的功能可以說非常之強大 。該類允許我們通過給attach方法傳入一個jvm的pid(程序id),遠端連線到jvm上 。代理類注入操作只是它眾多功能中的一個,通過loadAgent方法向jvm註冊一個代理程式agent,在該agent的代理程式中會得到一個Instrumentation例項,該例項可以 在class載入前改變class的位元組碼,也可以在class載入後重新載入。在呼叫Instrumentation例項的方法時,這些方法會使用ClassFileTransformer介面中提供的方法進行處理。
  2. VirtualMachineDescriptor 則是一個描述虛擬機器的容器類,配合 VirtualMachine 類完成各種功能。

attach實現動態注入的原理如下:

通過VirtualMachine類的attach(pid)方法,便可以attach到一個執行中的java程序上,之後便可以通過loadAgent(agentJarPath)來將agent的jar包注入到對應的程序,然後對應的程序會呼叫agentmain方法。

java agent使用全解析

既然是兩個程序之間通訊那肯定的建立起連線,VirtualMachine.attach動作類似TCP建立連線的三次握手,目的就是搭建attach通訊的連線。而後面執行的操作,例如vm.loadAgent,其實就是向這個socket寫入資料流,接收方target VM會針對不同的傳入資料來做不同的處理。

我們來測試一下agentmain的使用:

工程結構和 上面premain的測試一樣,編寫AgentMainTest,然後使用maven外掛打包 生成MANIFEST.MF。

package com.rickiyang.learn;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class AgentMainTest {

  public static void agentmain(String agentArgs,Instrumentation instrumentation) {
    instrumentation.addTransformer(new DefineTransformer(),true);
  }
  
  static class DefineTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader,byte[] classfileBuffer) throws IllegalClassFormatException {
      System.out.println("premain load Class:" + className);
      return classfileBuffer;
    }
  }
}
<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-jar-plugin</artifactId>
 <version>3.1.0</version>
 <configuration>
  <archive>
   <!--自動新增META-INF/MANIFEST.MF -->
   <manifest>
    <addClasspath>true</addClasspath>
   </manifest>
   <manifestEntries>
    <Agent-Class>com.rickiyang.learn.AgentMainTest</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
   </manifestEntries>
  </archive>
 </configuration>
</plugin>

將agent打包之後,就是編寫測試main方法。上面我們畫的圖中的步驟是:從一個attach JVM去探測目標JVM,如果目標JVM存在則向它傳送agent.jar。我測試寫的簡單了些,找到當前JVM並載入agent.jar。

package com.rickiyang.learn.job;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class TestAgentMain {

  public static void main(String[] args) throws IOException,AttachNotSupportedException,AgentLoadException,AgentInitializationException {
    //獲取當前系統中所有 執行中的 虛擬機器
    System.out.println("running JVM start ");
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    for (VirtualMachineDescriptor vmd : list) {
      //如果虛擬機器的名稱為 xxx 則 該虛擬機器為目標虛擬機器,獲取該虛擬機器的 pid
      //然後載入 agent.jar 傳送給該虛擬機器
      System.out.println(vmd.displayName());
      if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) {
        VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
        virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
        virtualMachine.detach();
      }
    }
  }

}

list()方法會去尋找當前系統中所有執行著的JVM程序,你可以列印vmd.displayName()看到當前系統都有哪些JVM程序在執行。因為main函式執行起來的時候程序名為當前類名,所以通過這種方式可以去找到當前的程序id。

注意:在mac上安裝了的jdk是能直接找到 VirtualMachine 類的,但是在windows中安裝的jdk無法找到,如果你遇到這種情況,請手動將你jdk安裝目錄下:lib目錄中的tools.jar新增進當前工程的Libraries中。

執行main方法的輸出為:

java agent使用全解析

可以看到實際上是啟動了一個socket程序去傳輸agent.jar。先列印了“running JVM start”表名main方法是先啟動了,然後才進入代理類的transform方法。

instrument原理

instrument的底層實現依賴於JVMTI(JVM Tool Interface),它是JVM暴露出來的一些供使用者擴充套件的介面集合,JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者去擴充套件自己的邏輯。JVMTIAgent是一個利用JVMTI暴露出來的介面提供了代理啟動時載入(agent on load)、代理通過attach形式載入(agent on attach)和代理解除安裝(agent on unload)功能的動態庫。而instrument agent可以理解為一類JVMTIAgent動態庫,別名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是專門為java語言編寫的插樁服務提供支援的代理。

啟動時載入instrument agent過程:

1.建立並初始化 JPLISAgent;

2.監聽 VMInit 事件,在 JVM 初始化完成之後做下面的事情:

  1. 建立 InstrumentationImpl 物件 ;
  2. 監聽 ClassFileLoadHook 事件 ;
  3. 呼叫 InstrumentationImpl 的loadClassAndCallPremain方法,在這個方法裡會去呼叫 javaagent 中 MANIFEST.MF 裡指定的Premain-Class 類的 premain 方法 ;

3.解析 javaagent 中 MANIFEST.MF 檔案的引數,並根據這些引數來設定 JPLISAgent 裡的一些內容。

執行時載入instrument agent過程:

通過 JVM 的attach機制來請求目標 JVM 載入對應的agent,過程大致如下:

1.建立並初始化JPLISAgent;

2.解析 javaagent 裡 MANIFEST.MF 裡的引數;

3.建立 InstrumentationImpl 物件;

4.監聽 ClassFileLoadHook 事件;

5.呼叫 InstrumentationImpl 的loadClassAndCallAgentmain方法,在這個方法裡會去呼叫javaagent裡 MANIFEST.MF 裡指定的Agent-Class類的agentmain方法。

Instrumentation的侷限性

大多數情況下,我們使用Instrumentation都是使用其位元組碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,但是有以下的侷限性:

1.premain和agentmain兩種方式修改位元組碼的時機都是類檔案載入之後,也就是說必須要帶有Class型別的引數,不能通過位元組碼檔案和自定義的類名重新定義一個本來不存在的類。

2.類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:

  1. 新類和老類的父類必須相同;
  2. 新類和老類實現的介面數也要相同,並且是相同的介面;
  3. 新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
  4. 新類和老類新增或刪除的方法必須是private static/final修飾的;
  5. 可以修改方法體。

除了上面的方式,如果想要重新定義一個類,可以考慮基於類載入器隔離的方式:建立一個新的自定義類載入器去通過新的位元組碼去定義一個全新的類,不過也存在只能通過反射呼叫該全新類的侷限性。

以上就是javaagent使用全解析的詳細內容,更多關於javaagent 使用的資料請關注我們其它相關文章!