无侵入式 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. 公司的 博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了 英文版 原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!