無侵入式 Java 探針能夠在不修改原始碼、不載入 -javaagent、不重啟程序的前提下,捕獲執行中 JVM 的真實行為——方法呼叫、引數值、物件欄位。與只顯示時間消耗位置的取樣式 profiler(async-profiler、JFR),以及注入位元組碼的插樁式工具(Arthas、BTrace)不同,OpenResty XRay 透過動態追蹤從外部讀取 JVM 執行時狀態,在解釋執行和 JIT 編譯的方法上都能捕獲每次呼叫的實參值,且零程式碼修改。

本文講清它的原理,並端到端演示如何從執行中的 Java 程序捕獲方法引數。

為甚麼無侵入式 Java 探針在生產環境很重要

Java 應用一旦進入生產環境,很多問題就變得"隱身"了:日誌有限,效能分析工具不能隨便上,偵錯程式更是想都別想。可業務還在跑,使用者還在訪問——你既不能停機,也不能改原始碼。

傳統 Java profiler 各有短板。侵入式方法——加日誌、打斷點、帶 instrumentation 重新編譯——給你細節,代價是必須改程式碼、重新發布。取樣式 profiler(async-profiler、JFR)能安全跑在生產上,但只告訴你時間花在哪裡,無法告訴你函式被呼叫時傳入了甚麼值。無侵入式函式探針填補了這個空白:它們能動態捕獲函式級的關鍵執行資訊——包括每次呼叫的實參值——且不打擾系統正常執行。

效能診斷。 Java 應用中的效能瓶頸往往藏得很深:一次資料庫的慢呼叫、一個高頻但低效的工具方法。函式探針能在執行時直接捕獲目標方法的呼叫時間、入參和出參,就像給開發者配了一臺"顯微鏡",把問題精確到函式級——與請求級延遲分析形成互補,提供每次呼叫的可見性。

生產環境的保險繩。 “測試環境正常,生產環境崩了"是每個技術團隊的噩夢,但你絕不可能在高峰期隨意重啟或新增除錯程式碼。無侵入式探針可以動態掛載/解除安裝,做到"用完即走”——既能獲取線索,又不會給線上帶來額外風險。

安全與合規。 在涉及加密、認證、資金交易等敏感場景下,團隊需要實時監控呼叫情況。探針能在不暴露核心原始碼邏輯的前提下,提供關鍵函式呼叫軌跡的深度可見性,為合規審計和安全防護提供第一手資料。

複雜環境除錯。 IDE、斷點在容器化、分散式、雲原生環境下往往行不通。函式探針提供了一種更輕量的方式,讓你在傳統除錯手段失效的複雜環境中依然能捕捉最關鍵的函式級資訊。

實戰演示:在生產環境捕獲 Java 方法引數

讓我們透過一個具體的例子來演示如何使用函式探針。假設我們有以下 Java 方法正在執行:

public class UserService {
    public static class User {
        private String email;

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }
    }

    public String greetUser(String name, int age, User user) {
        return "Hello " + name;
    }
}

Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        User user = new User();
        user.setEmail("tom@example.com");
        userService.greetUser("Tom", 25, user);
    }
}, 0, 1000);

這個方法接收三個引數:字串型別的 name、整數型別的 age,以及物件型別的 user。我們的目標是在不修改程式碼的情況下,監控這些引數的值。

步驟一:獲取函式入口地址

首先,我們需要使用 ylang 來獲取目標方法的入口地址:

_probe _process.begin {
    find_method_entry("UserService", "greetUser", "(Ljava/lang/String;ILUserService$User;)Ljava/lang/String;");
    _exit();
}

分別傳入類名,方法名,和方法簽名。

這個命令會返回 greetUser 方法在記憶體中的入口地址,這是設定探針的關鍵資訊。

type: compiled, class: UserService, method: greetUser, signature: (Ljava/lang/String;ILUserService$User;)Ljava/lang/String;, entry: 0x7fffe0d2e20c, code_begin: 0x7fffe0d2e1e0, code_end: 0x7fffe0d2e7a0
type: interpreted, class: UserService, method: greetUser, signature: (Ljava/lang/String;ILUserService$User;)Ljava/lang/String;, entry: 0x7fffe046b540, code_begin: 0x7fff6b400730, code_end: 0x7fff6b400749

步驟二:設定函式探針

接下來,我們使用獲取到的 JIT 方法入口地址作為 watchpoint 地址,設定方法呼叫攔截器:

#include "jvm.y"

_probe _watchpoint(0x7fffe0d2e20c).exec
{
    _str name = get_java_string(nmethod_oop_arg(2));
    int age = nmethod_int_arg(3);
    oop email_obj = dump_field_object(nmethod_oop_arg(4), "email");
    _str email = get_java_string(email_obj);
    printf("name: %s, age: %d, email: %s\n", name, age, email);
    _exit();
}

這段探針程式碼的主要功能,是在 Java 方法被呼叫時,自動捕獲其引數資訊,從而幫助開發者在不中斷服務的情況下分析函式行為。具體來說,它的執行流程可以分為以下幾個關鍵步驟:

  • _watchpoint(ENTRY).exec:在目標方法入口處設定監控點,一旦方法被呼叫,就會觸發探針邏輯。

  • nmethod_oop_arg(2)nmethod_int_arg(3)nmethod_oop_arg(4):依次獲取被呼叫方法的第 2、3、4 個引數。

  • 需要注意的是,在 Java 例項方法中,第 1 個引數預設為 this 物件,因此實際的業務引數從索引 2 開始計算。

  • get_java_string():用於將 Java 層的字串物件讀取為可列印的字串值。

  • nmethod_int_arg():用於提取整數型別的引數值。

  • dump_field_object(user_obj, "email"):從 User 物件中取出 email 欄位對應的物件。

  • get_java_string(email_obj):將提取到的 email 欄位物件轉換為字串,以便日誌輸出或後續分析。

透過這段探針邏輯,我們能夠在不修改原始碼的前提下,實時捕獲 Java 方法的引數值,並進一步提取物件內部的關鍵欄位,為問題排查和行為分析提供極高的可觀測性。

執行結果

目標方法一旦執行,ylang 執行器會輸出以下監控結果:

name: Tom, age: 25, email: tom@example.com

OpenResty XRay 與常見 Java Profiler 的差異

你剛才看到 XRay 透過幾行 ylang 就能在執行中的 JVM 上抓取每次呼叫的實參值——沒有載入 -javaagent、沒有改寫位元組碼、沒有重啟程序。下面把這種方式和其他同樣"不改原始碼"的 Java profiler(Arthas、BTrace、JMC Agent、async-profiler、JFR)做個對比:

  • -javaagent、無位元組碼修改。 Arthas 和 BTrace 需要載入 -javaagent 並注入位元組碼;XRay 從程序外部讀取 JVM 執行時狀態。
  • 捕獲每次呼叫的實參值。 async-profiler 和 JFR 只做棧取樣——它們能告訴你哪些方法熱,但看不到傳入的值。XRay 能在每次呼叫時抓取實際的引數和物件欄位。
  • 同時覆蓋解釋執行和 JIT 編譯的方法,且執行時可動態啟停。

一個注意點:行內函數由於沒有獨立的函式入口,無法透過這種方式監控。

常見問題

使用 OpenResty XRay 需要載入 -javaagent 嗎?

不需要。OpenResty XRay 從程序外部讀取 JVM 執行時狀態,無需新增 -javaagent 引數、無需預載入 bootstrap JAR,啟停探針也不需要重啟程序。

需要位元組碼修改(instrumentation)嗎?

不需要。XRay 不修改也不重寫 JVM 位元組碼。方法探針設定在解釋執行和 JIT 編譯方法的入口地址上,執行程式的位元組碼不受影響。

和 Arthas 的 watch 命令有甚麼區別?

Arthas 會向目標程序載入 -javaagent,並透過位元組碼增強來攔截方法呼叫。OpenResty XRay 則從程序外部觀察 JVM,不載入 -javaagent、不修改位元組碼,同時能在解釋執行和 JIT 編譯方法上都捕獲每次呼叫的實參值。

能否捕獲 JIT 編譯方法的引數值?

可以。如上文演示所示,find_method_entry 返回的入口地址同時包含 interpretedcompiled(JIT)兩種版本,_watchpoint(...).exec 探針在任一路徑上觸發時都能捕獲實際傳入的實參值。

關於 OpenResty XRay

OpenResty XRay 是一個動態追蹤產品,它可以自動分析執行中的應用,以解決效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+、eBPF+、GDB 和 ODB。

關於作者

章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴,是 “邊緣計算 “、” 動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用 動態追蹤 技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的 部落格網站 。也歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號

翻譯

我們提供了 英文版 原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!