欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

字節(jié)碼調(diào)教入口JVM?寄生插件javaagent

 更新時(shí)間:2023年08月23日 17:16:48   作者:悅  
這篇文章主要介紹了字節(jié)碼調(diào)教入口JVM?寄生插件javaagent方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

Java Instrumentation 包

Java Instrumentation 概述

Java Instrumentation 這個(gè)技術(shù)看起來非常神秘,很少有書會(huì)詳細(xì)介紹。但是有很多工具是基于 Instrumentation 來實(shí)現(xiàn)的:

  • APM 產(chǎn)品: pinpoint、skywalking、newrelic、聽云的 APM 產(chǎn)品等都基于 Instrumentation 實(shí)現(xiàn)
  • 熱部署工具:Intellij idea 的 HotSwap、Jrebel 等
  • Java 診斷工具:Arthas、Btrace 等

由于對字節(jié)碼修改功能的巨大需求,JDK 從 JDK5 版本開始引入了java.lang.instrument 包。它可以通過 addTransformer 方法設(shè)置一個(gè) ClassFileTransformer,可以在這個(gè) ClassFileTransformer 實(shí)現(xiàn)類的轉(zhuǎn)換。

JDK 1.5 支持靜態(tài) Instrumentation,基本的思路是在 JVM 啟動(dòng)的時(shí)候添加一個(gè)代理(javaagent),每個(gè)代理是一個(gè) jar 包,其 MANIFEST.MF 文件里指定了代理類,這個(gè)代理類包含一個(gè) premain 方法。JVM 在類加載時(shí)候會(huì)先執(zhí)行代理類的 premain 方法,再執(zhí)行 Java 程序本身的 main 方法,這就是 premain 名字的來源。在 premain 方法中可以對加載前的 class 文件進(jìn)行修改。這種機(jī)制可以認(rèn)為是虛擬機(jī)級別的 AOP,無需對原有應(yīng)用做任何修改,就可以實(shí)現(xiàn)類的動(dòng)態(tài)修改和增強(qiáng)。

從 JDK 1.6 開始支持更加強(qiáng)大的動(dòng)態(tài) Instrument,在JVM 啟動(dòng)后通過 Attach API 遠(yuǎn)程加載,后面會(huì)詳細(xì)介紹。

本文會(huì)分為 javaagent 和動(dòng)態(tài) Attach 兩個(gè)部分來介紹

Java Instrumentation 核心方法

Instrumentation 是 java.lang.instrument 包下的一個(gè)接口,這個(gè)接口的方法提供了注冊類文件轉(zhuǎn)換器、獲取所有已加載的類等功能,允許我們在對已加載和未加載的類進(jìn)行修改,實(shí)現(xiàn) AOP、性能監(jiān)控等功能。

常用的方法如下:

/**
 * 為 Instrumentation 注冊一個(gè)類文件轉(zhuǎn)換器,可以修改讀取類文件字節(jié)碼
 */
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
 * 對JVM已經(jīng)加載的類重新觸發(fā)類加載
 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
 * 獲取當(dāng)前 JVM 加載的所有類對象
 */
Class[] getAllLoadedClasses()

它的 addTransformer 給 Instrumentation 注冊一個(gè) transformer,transformer 是 ClassFileTransformer 接口的實(shí)例,這個(gè)接口就只有一個(gè) transform 方法,調(diào)用 addTransformer 設(shè)置 transformer 以后,后續(xù)JVM 加載所有類之前都會(huì)被這個(gè) transform 方法攔截,這個(gè)方法接收原類文件的字節(jié)數(shù)組,返回轉(zhuǎn)換過的字節(jié)數(shù)組,在這個(gè)方法中可以做任意的類文件改寫。

下面是一個(gè)空的 ClassFileTransformer 的實(shí)現(xiàn):

public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
        // 在這里讀取、轉(zhuǎn)換類文件
        return classBytes;
    }
}

接下來我們來介紹本文的主角之一 javaagent。

Javaagent 介紹

Javaagent 是一個(gè)特殊的 jar 包,它并不能單獨(dú)啟動(dòng)的,而必須依附于一個(gè) JVM 進(jìn)程,可以看作是 JVM 的一個(gè)寄生插件,使用 Instrumentation 的 API 用來讀取和改寫當(dāng)前 JVM 的類文件。

Agent 的兩種使用方式

它有兩種使用方式:

  • 在 JVM 啟動(dòng)的時(shí)候加載,通過 javaagent 啟動(dòng)參數(shù) java -javaagent:myagent.jar MyMain,這種方式在程序 main 方法執(zhí)行之前執(zhí)行 agent 中的 premain 方法

在 JVM 啟動(dòng)后 Attach,通過 Attach API 進(jìn)行加載,這種方式會(huì)在 agent 加載以后執(zhí)行 agentmain 方法
premain 和 agentmain 方法簽名如下:

public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception

public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception

這兩個(gè)方法都有兩個(gè)參數(shù)

  • 第一個(gè) agentArgument 是 agent 的啟動(dòng)參數(shù),可以在 JVM 啟動(dòng)命令行中設(shè)置,比如java -javaagent:<jarfile>=appId:agent-demo,agentType:singleJar test.jar的情況下 agentArgument 的值為 "appId:agent-demo,agentType:singleJar"。
  • 第二個(gè) instrumentation 是 java.lang.instrument.Instrumentation 的實(shí)例,可以通過 addTransformer 方法設(shè)置一個(gè) ClassFileTransformer。

第一種 premain 方式的加載時(shí)序如下:

Agent 打包

為了能夠以 javaagent 的方式運(yùn)行 premain 和 agentmain 方法,我們需要將其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一個(gè)典型的生成好的 MANIFEST.MF 內(nèi)容如下

為了能夠以 javaagent 的方式運(yùn)行 premain 和 agentmain 方法,我們需要將其打包成 jar 包,并在其中的 MANIFEST.MF 配置文件中,指定 Premain-class 等信息,一個(gè)典型的生成好的 MANIFEST.MF 內(nèi)容如下

下面是一個(gè)可以幫助生成上面 MANIFEST.MF 的 maven 配置

<build>
  <finalName>my-javaagent</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifestEntries>
            <Agent-Class>me.geek01.javaagent.AgentMain</Agent-Class>
            <Premain-Class>me.geek01.javaagent.AgentMain</Premain-Class>
            <Can-Redefine-Classes>true</Can-Redefine-Classes>
            <Can-Retransform-Classes>true</Can-Retransform-Classes>
          </manifestEntries>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

Agent 使用方式一:JVM 啟動(dòng)參數(shù)

下面使用 javaagent 實(shí)現(xiàn)簡單的函數(shù)調(diào)用棧跟蹤,以下面的代碼為例:

public class MyTest {
    public static void main(String[] args) {
        new MyTest().foo();
    }
    public void foo() {
        bar1();
        bar2();
    }

    public void bar1() {
    }

    public void bar2() {
    }
}

通過 javaagent 啟動(dòng)參數(shù)的方式在每個(gè)函數(shù)進(jìn)入和結(jié)束時(shí)都打印一行日志,實(shí)現(xiàn)調(diào)用過程的追蹤的效果。

核心的方法 instrument 的邏輯如下:

public static class MyMethodVisitor extends AdviceAdapter {
    @Override
    protected void onMethodEnter() {
        // 在方法開始處插入 <<<enter xxx
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("<<<enter " + this.getName());
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        super.onMethodEnter();
    }
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        // 在方法結(jié)束處插入 <<<exit xxx
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn(">>>exit " + this.getName());
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
}

把 agent 打包生成 my-trace-agent.jar,添加 agent 啟動(dòng) MyTest 類

java -javaagent:/path_to/my-trace-agent.jar MyTest

可以看到輸出結(jié)果如下:

<<<enter main
<<<enter foo
<<<enter bar1
>>>exit bar1
<<<enter bar2
>>>exit bar2
>>>exit foo
>>>exit main

通過上面的方式,我們在不修改 MyTest 類源碼的情況下實(shí)現(xiàn)了調(diào)用鏈跟蹤的效果。更加健壯和完善的調(diào)用鏈跟蹤實(shí)現(xiàn)會(huì)在后面的 APM 章節(jié)詳細(xì)介紹。

Agent 使用方式二:Attach API 使用

在 JDK5 中,開發(fā)者只能 JVM 啟動(dòng)時(shí)指定一個(gè) javaagent 在 premain 中操作字節(jié)碼,Instrumentation 也僅限于 main 函數(shù)執(zhí)行前,這樣的方式存在一定的局限性。從 JDK6 開始引入了動(dòng)態(tài) Attach Agent 的方案,除了在命令行中指定 javaagent,現(xiàn)在可以通過 Attach API 遠(yuǎn)程加載。我們常用的 jstack、arthas 等工具都是通過 Attach 機(jī)制實(shí)現(xiàn)的。

接下來我們會(huì)結(jié)合跨進(jìn)程通信中的信號(hào)和 Unix 域套接字來看 JVM Attach API 的實(shí)現(xiàn)原理

JVM Attach API 基本使用

下面以一個(gè)實(shí)際的例子來演示動(dòng)態(tài) Attach API 的使用,代碼中有一個(gè) main 方法,每隔 3s 輸出 foo 方法的返回值 100,接下來動(dòng)態(tài) Attach 上 MyTestMain 進(jìn)程,修改 foo 的字節(jié)碼,讓 foo 方法返回 50。

public class MyTestMain {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            System.out.println(foo());
            TimeUnit.SECONDS.sleep(3);
        }
    }

    public static int foo() {
        return 100; // 修改后 return 50;
    }
}

步驟如下:

1、編寫 Attach Agent,對 foo 方法做注入,完整的代碼見:github.com/arthur-zhan…

動(dòng)態(tài) Attach 的 agent 與通過 JVM 啟動(dòng) javaagent 參數(shù)指定的 agent jar 包的方式有所不同,動(dòng)態(tài) Attach 的 agent 會(huì)執(zhí)行 agentmain 方法,而不是 premain 方法。

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println("agentmain called");
        inst.addTransformer(new MyClassFileTransformer(), true);
        Class classes[] = inst.getAllLoadedClasses();
        for (int i = 0; i &lt; classes.length; i++) {
            if (classes[i].getName().equals("MyTestMain")) {
                System.out.println("Reloading: " + classes[i].getName());
                inst.retransformClasses(classes[i]);
                break;
            }
        }
    }
}

2、因?yàn)槭强邕M(jìn)程通信,Attach 的發(fā)起端是一個(gè)獨(dú)立的 java 程序,這個(gè) java 程序會(huì)調(diào)用 VirtualMachine.attach 方法開始和目標(biāo) JVM 進(jìn)行跨進(jìn)程通信。

public class MyAttachMain {
    public static void main(String[] args) throws Exception {
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        try {
            vm.loadAgent("/path/to/agent.jar");
        } finally {
            vm.detach();
        }
    }
}

使用 jps 查詢到 MyTestMain 的進(jìn)程 id,

java -cp /path/to/your/tools.jar:. MyAttachMain pid

可以看到 MyTestMain 的輸出的 foo 方法已經(jīng)返回了 50。

java -cp . MyTestMain
100
100
100
agentmain called
Reloading: MyTestMain
50
50
50

JVM Attach API 的底層原理

JVM Attach API 的實(shí)現(xiàn)主要基于信號(hào)和 Unix 域套接字,接下來詳細(xì)介紹這兩部分的內(nèi)容。

信號(hào)是什么

信號(hào)是某事件發(fā)生時(shí)對進(jìn)程的通知機(jī)制,也被稱為“軟件中斷”。信號(hào)可以看做是一種非常輕量級的進(jìn)程間通信,信號(hào)由一個(gè)進(jìn)程發(fā)送給另外一個(gè)進(jìn)程,只不過是經(jīng)由內(nèi)核作為一個(gè)中間人發(fā)出,信號(hào)最初的目的是用來指定殺死進(jìn)程的不同方式。

每個(gè)信號(hào)都有一個(gè)名字,以 "SIG" 開頭,最熟知的信號(hào)應(yīng)該是 SIGINT,我們在終端執(zhí)行某個(gè)應(yīng)用程序的過程中按下 Ctrl+C 一般會(huì)終止正在執(zhí)行的進(jìn)程,正是因?yàn)榘聪?nbsp;Ctrl+C 會(huì)發(fā)送 SIGINT 信號(hào)給目標(biāo)程序。

每個(gè)信號(hào)都有一個(gè)唯一的數(shù)字標(biāo)識(shí),從 1 開始,下面是常見的信號(hào)量列表:

在 Linux 中,一個(gè)前臺(tái)進(jìn)程可以使用 Ctrl+C 進(jìn)行終止,對于后臺(tái)進(jìn)程需要使用 kill 加進(jìn)程號(hào)的方式來終止,kill 命令是通過發(fā)送信號(hào)給目標(biāo)進(jìn)程來實(shí)現(xiàn)終止進(jìn)程的功能。默認(rèn)情況下,kill 命令發(fā)送的是編號(hào)為 15 的 SIGTERM 信號(hào),這個(gè)信號(hào)可以被進(jìn)程捕獲,選擇忽略或正常退出。目標(biāo)進(jìn)程如果沒有自定義處理這個(gè)信號(hào),就會(huì)被終止。對于那些忽略 SIGTERM 信號(hào)的進(jìn)程,則需要編號(hào)為 9 的 SIGKILL 信號(hào)強(qiáng)行殺死進(jìn)程,SIGKILL 信號(hào)不能被忽略也不能被捕獲和自定義處理。

下面寫了一段 C 代碼,自定義處理了 SIGQUIT、SIGINT、SIGTERM 信號(hào)

signal.c
static void signal_handler(int signal_no) {
    if (signal_no == SIGQUIT) {
        printf("quit signal receive: %d\n", signal_no);
    } else if (signal_no == SIGTERM) {
        printf("term signal receive: %d\n", signal_no);
    } else if (signal_no == SIGINT) {
        printf("interrupt signal receive: %d\n", signal_no);
    }
}
int main() {
    signal(SIGQUIT, signal_handler);
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    for (int i = 0;; i++) {
        printf("%d\n", i);
        sleep(3);
    }
}

編譯運(yùn)行上面的 signal.c 文件

gcc signal.c -o signal
./signal

這種情況下,在終端中Ctrl+C,kill -3,kill -15都沒有辦法殺掉這個(gè)進(jìn)程,只能用kill -9

0
^Cinterrupt signal receive: 2     // Ctrl+C
1
2
term signal receive: 15           // kill pid
3
4
5
quit signal receive: 3             // kill -3 
6
7
8
[1]    46831 killed     ./signal  // kill -9 成功殺死進(jìn)程

JVM 對 SIGQUIT 的默認(rèn)行為是打印所有運(yùn)行線程的堆棧信息,在類 Unix 系統(tǒng)中,可以通過使用命令 kill -3 pid 來發(fā)送 SIGQUIT 信號(hào)。運(yùn)行上面的 MyTestMain,使用 jps 找到整個(gè) JVM 的進(jìn)程 id,執(zhí)行 kill -3 pid,在終端就可以看到打印了所有的線程的調(diào)用棧信息:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.51-b03 mixed mode):
"Service Thread" #8 daemon prio=9 os_prio=31 tid=0x00007fe060821000 nid=0x4403 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
...
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fe061008800 nid=0x3403 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
"main" #1 prio=5 os_prio=31 tid=0x00007fe060003800 nid=0x1003 waiting on condition [0x000070000d203000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at MyTestMain.main(MyTestMain.java:10)

Unix 域套接字(Unix Domain Socket)

使用 TCP 和 UDP 進(jìn)行 socket 通信是一種廣為人知的 socket 使用方式,除了這種方式還有一種稱為 Unix 域套接字的方式,可以實(shí)現(xiàn)同一主機(jī)上的進(jìn)程間通信。雖然使用 127.0.0.1 環(huán)回地址也可以通過網(wǎng)絡(luò)實(shí)現(xiàn)同一主機(jī)的進(jìn)程間通信,但 Unix 域套接字更可靠、效率更高。Docker 守護(hù)進(jìn)程(Docker daemon)使用了 Unix 域套接字,容器中的進(jìn)程可以通過它與Docker 守護(hù)進(jìn)程進(jìn)行通信。MySQL 同樣提供了域套接字進(jìn)行訪問的方式。

Unix 域套接字是什么?

Unix 域套接字是一個(gè)文件,通過 ls 命令可以看到

ls -l
srwxrwxr-x. 1 ya ya        0 9月   8 00:26 tmp.sock

兩個(gè)進(jìn)程通過讀寫這個(gè)文件就實(shí)現(xiàn)了進(jìn)程間的信息傳遞。文件的擁有者和權(quán)限決定了誰可以讀寫這個(gè)套接字。

與普通套接字的區(qū)別是什么?

  • Unix 域套接字更加高效,Unix 套接字不用進(jìn)行協(xié)議處理,不需要計(jì)算序列號(hào),也不需要發(fā)送確認(rèn)報(bào)文,只需要復(fù)制數(shù)據(jù)即可
  • Unix 域套接字是可靠的,不會(huì)丟失報(bào)文,普通套接字是為不可靠通信設(shè)計(jì)的
  • Unix 域套接字的代碼可以非常簡單的修改轉(zhuǎn)為普通套接字

下面是一個(gè)簡單的 C 實(shí)現(xiàn)的域套接字的例子

.
├── client.c
└── server.c

server.c 充當(dāng) Unix 域套接字服務(wù)器,啟動(dòng)后會(huì)在當(dāng)前目錄生成一個(gè)名為 tmp.sock 的 Unix 域套接字文件,它讀取客戶端寫入的內(nèi)容并輸出。

server.c
int main() {
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "tmp.sock");
    int ret = bind(fd, (struct sockaddr *) &addr, sizeof(addr));
    listen(fd, 5)
    int accept_fd;
    char buf[100];
    while (1) {
        accept_fd = accept(fd, NULL, NULL)) == -1);
        while ((ret = read(accept_fd, buf, sizeof(buf))) > 0) {
            // 輸出客戶端傳過來的數(shù)據(jù)
            printf("receive %u bytes: %s\n", ret, buf);
        }
}

客戶端的代碼如下:

client.c
int main() {
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "tmp.sock");
    connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1
    int rc;
    char buf[100];
    // 讀取終端標(biāo)準(zhǔn)輸入的內(nèi)容,寫入到 Unix 域套接字文件中
    while ((rc = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
        write(fd, buf, rc);
    }
}

在命令行中進(jìn)行編譯和執(zhí)行

gcc server.c -o server
gcc client.c -o client

啟動(dòng)兩個(gè)終端,一個(gè)啟動(dòng) server 端,一個(gè)啟動(dòng) client 端

./server
./client

可以看到當(dāng)前目錄生成了一個(gè) "tmp.sock" 文件

ls -l
srwxrwxr-x. 1 ya ya    0 9月   8 00:08 tmp.sock

在 client 輸入 hello,在 server 的終端就可以看到

./server
receive 6 bytes: hello

JVM Attach 過程分析

執(zhí)行 MyAttachMain,當(dāng)指定一個(gè)不存在的 JVM 進(jìn)程時(shí),會(huì)出現(xiàn)如下的錯(cuò)誤:

java -cp /path/to/your/tools.jar:. MyAttachMain 1234
Exception in thread "main" java.io.IOException: No such process
    at sun.tools.attach.LinuxVirtualMachine.sendQuitTo(Native Method)
    at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:91)
    at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
    at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
    at MyAttachMain.main(MyAttachMain.java:8)

可以看到 VirtualMachine.attach 最終調(diào)用了 sendQuitTo 方法,這是一個(gè) native 的方法,底層就是發(fā)送了 SIGQUIT 信號(hào)給目標(biāo) JVM 進(jìn)程。

前面信號(hào)部分我們介紹過,JVM 對 SIGQUIT 的默認(rèn)行為是 dump 當(dāng)前的線程堆棧,那為什么調(diào)用 VirtualMachine.attach 沒有輸出調(diào)用棧堆棧呢?

對于 Attach 的發(fā)起方,假設(shè)目標(biāo)進(jìn)程為 12345,這部分的詳細(xì)的過程如下:

1、Attach 端檢查臨時(shí)文件目錄是否有 .java_pid12345 文件

這個(gè)文件是一個(gè) UNIX 域套接字文件,由 Attach 成功以后的目標(biāo) JVM 進(jìn)程生成。如果這個(gè)文件存在,說明正在 Attach 中,可以用這個(gè) socket 進(jìn)行下一步的通信。如果這個(gè)文件不存在則創(chuàng)建一個(gè) .attach_pid12345 文件,這部分的偽代碼如下:

String tmpdir = "/tmp";
File socketFile = new File(tmpdir,  ".java_pid" + pid);
if (socketFile.exists()) {
    File attachFile = new File(tmpdir, ".attach_pid" + pid);
    createAttachFile(attachFile.getPath());
}

2、Attach 端檢查如果沒有 .java_pid12345 文件,創(chuàng)建完 .attach_pid12345 文件以后發(fā)送 SIGQUIT 信號(hào)給目標(biāo) JVM。然后每隔 200ms 檢查一次 socket 文件是否已經(jīng)生成,5s 以后還沒有生成則退出,如果有生成則進(jìn)行 socket 通信

3、對于目標(biāo) JVM 進(jìn)程而言,它的 Signal Dispatcher 線程收到 SIGQUIT 信號(hào)以后,會(huì)檢查 .attach_pid12345 文件是否存在。

  • 目標(biāo) JVM 如果發(fā)現(xiàn) .attach_pid12345 不存在,則認(rèn)為這不是一個(gè) attach 操作,執(zhí)行默認(rèn)行為,輸出當(dāng)前所有線程的堆棧

目標(biāo) JVM 如果發(fā)現(xiàn) .attach_pid12345 存在,則認(rèn)為這是一個(gè) attach 操作,會(huì)啟動(dòng) Attach Listener 線程,負(fù)責(zé)處理 Attach 請求,同時(shí)創(chuàng)建名為 .java_pid12345 的 socket 文件,監(jiān)聽 socket。

源碼中 /hotspot/src/share/vm/runtime/os.cpp 這一部分處理的邏輯如下:

#define SIGBREAK SIGQUIT
static void signal_thread_entry(JavaThread* thread, TRAPS) {
while (true) {
  int sig;
  {
  switch (sig) {
    case SIGBREAK: { 
      // Check if the signal is a trigger to start the Attach Listener - in that
      // case don't print stack traces.
      if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
        continue;
      }
      ...
      // Print stack traces
  }
}

AttachListener 的 is_init_trigger 在 .attach_pid12345 文件存在的情況下會(huì)新建 .java_pid12345 套接字文件,同時(shí)監(jiān)聽此套接字,準(zhǔn)備 Attach 端發(fā)送數(shù)據(jù)。

那 Attach 端和目標(biāo)進(jìn)程用 socket 傳遞了什么信息呢?可以通過 strace 的方式看到 Attach 端究竟往 socket 里面寫了什么:

sudo strace -f java -cp /usr/local/jdk/lib/tools.jar:. MyAttachMain 12345  2> strace.out
...
5841 [pid  3869] socket(AF_LOCAL, SOCK_STREAM, 0) = 5
5842 [pid  3869] connect(5, {sa_family=AF_LOCAL, sun_path="/tmp/.java_pid12345"}, 110)      = 0
5843 [pid  3869] write(5, "1", 1)            = 1
5844 [pid  3869] write(5, "\0", 1)           = 1
5845 [pid  3869] write(5, "load", 4)         = 4
5846 [pid  3869] write(5, "\0", 1)           = 1
5847 [pid  3869] write(5, "instrument", 10)  = 10
5848 [pid  3869] write(5, "\0", 1)           = 1
5849 [pid  3869] write(5, "false", 5)        = 5
5850 [pid  3869] write(5, "\0", 1)           = 1
5855 [pid  3869] write(5, "/home/ya/agent.jar"..., 18 <unfinished ...>

可以看到往 socket 寫入的內(nèi)容如下:

1
\0
load
\0
instrument
\0
false
\0
/home/ya/agent.jar
\0

數(shù)據(jù)之間用 \0 字符分隔,第一行的 1 表示協(xié)議版本,接下來是發(fā)送指令 "load instrument false /home/ya/agent.jar" 給目標(biāo) JVM,目標(biāo) JVM 收到這些數(shù)據(jù)以后就可以加載相應(yīng)的 agent jar 包進(jìn)行字節(jié)碼的改寫。

如果從 socket 的角度來看,VirtualMachine.attach 方法相當(dāng)于三次握手建連,VirtualMachine.loadAgent 則是握手成功之后發(fā)送數(shù)據(jù),VirtualMachine.detach 相當(dāng)于四次揮手?jǐn)嚅_連接。

這個(gè)過程如下圖所示:

小結(jié)

本文講解了 javaagent,一起來回顧一下要點(diǎn):

  • 第一,javaagent 是一個(gè)使用 instrumentation 的 API 用來改寫類文件的 jar 包,可以看作是 JVM 的一個(gè)寄生插件。
  • 第二,javaagent 有兩個(gè)重要的入口類:Premain-Class 和 Agent-Class,分別對應(yīng)入口函數(shù) premain 和 agentmain,其中 agentmain 可以采用遠(yuǎn)程 attach API 的方式遠(yuǎn)程掛載另一個(gè) JVM 進(jìn)程。

以上就是字節(jié)碼調(diào)教入口JVM 寄生插件javaagent的詳細(xì)內(nèi)容,更多關(guān)于JVM javaagent字節(jié)碼的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • SpringBoot 單元測試JUnit的使用詳解

    SpringBoot 單元測試JUnit的使用詳解

    這篇文章主要介紹了SpringBoot 單元測試JUnit的使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • java 設(shè)計(jì)模式之依賴倒置實(shí)例詳解

    java 設(shè)計(jì)模式之依賴倒置實(shí)例詳解

    這篇文章主要介紹了java 設(shè)計(jì)模式之依賴倒置,結(jié)合實(shí)例形式詳細(xì)分析了依賴倒置的相關(guān)概念、原理、使用技巧及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下
    2019-11-11
  • Java maven詳細(xì)介紹

    Java maven詳細(xì)介紹

    今天給大家復(fù)習(xí)一下Java基礎(chǔ)知識(shí),簡單介紹Maven,文中有非常詳細(xì)的解釋,對Java初學(xué)者很有幫助喲,需要的朋友可以參考下,希望能夠給你帶來幫助
    2021-09-09
  • Spring中的@PropertySource注解源碼詳解

    Spring中的@PropertySource注解源碼詳解

    這篇文章主要介紹了Spring中的@PropertySource注解源碼詳解,@PropertySource注解用于指定資源文件讀取的位置,它不僅能讀取properties文件,也能讀取xml文件,并且通過yaml解析器,配合自定義PropertySourceFactory實(shí)現(xiàn)解析yaml文件,需要的朋友可以參考下
    2023-11-11
  • 關(guān)于Kafka消費(fèi)者訂閱方式

    關(guān)于Kafka消費(fèi)者訂閱方式

    這篇文章主要介紹了關(guān)于Kafka消費(fèi)者訂閱方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • Java 變量類型及其實(shí)例

    Java 變量類型及其實(shí)例

    這篇文章主要講解Java中變量的類型以及實(shí)例,希望能給大家做一個(gè)參考
    2017-04-04
  • Java+opencv3.2.0實(shí)現(xiàn)hough直線檢測

    Java+opencv3.2.0實(shí)現(xiàn)hough直線檢測

    這篇文章主要為大家詳細(xì)介紹了Java+opencv3.2.0之hough直線檢測,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • Java利用異常中斷當(dāng)前任務(wù)的技巧分享

    Java利用異常中斷當(dāng)前任務(wù)的技巧分享

    在日常開發(fā)中,我們經(jīng)常遇到調(diào)用別人的代碼來完成某個(gè)任務(wù),但是當(dāng)代碼比較耗時(shí)的時(shí)候,沒法從外部終止該任務(wù),所以本文為大家介紹了如何利用異常中斷當(dāng)前任務(wù),需要的可以參考下
    2023-08-08
  • IDEA中Directory創(chuàng)建多級目錄的實(shí)現(xiàn)

    IDEA中Directory創(chuàng)建多級目錄的實(shí)現(xiàn)

    本文主要介紹了IDEA中Directory創(chuàng)建多級目錄的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • SpringBoot獲取ApplicationContext的3種方式

    SpringBoot獲取ApplicationContext的3種方式

    這篇文章主要為大家詳細(xì)介紹了SpringBoot獲取ApplicationContext的3種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-09-09

最新評論