A non-invasive Java profiler captures what a running JVM is actually doing—method calls, argument values, object fields—without modifying source code, adding a -javaagent, or restarting the process. Unlike sampling profilers (async-profiler, JFR) that only show where time is spent, and instrumentation-based tools (Arthas, BTrace) that inject bytecode, OpenResty XRay reads live JVM state from the outside using dynamic tracing, capturing per-call argument values on both interpreted and JIT-compiled methods with zero code change.

This post walks through how it works, and demonstrates capturing method parameters from a live Java process end-to-end.

Why Non-Invasive Java Profiling Matters in Production

Once a Java application enters production, problems become elusive: logs are limited, performance-analysis tools are hard to deploy, and debuggers aren’t an option. Yet the business keeps running—you can’t shut down, and you can’t easily modify source.

Traditional Java profilers force a tradeoff. Intrusive approaches—adding logs, breakpoints, or recompiling with instrumentation—give you detail but require code changes and redeployment. Sampling profilers like async-profiler and JFR run safely in production but only show where time is spent, not what values flowed through a function. Non-invasive function probes fill this gap: they dynamically capture critical function-level runtime information—including per-call argument values—without disturbing normal operation.

Performance diagnosis. Bottlenecks in Java applications are often deeply hidden—a slow database call, a high-frequency but inefficient utility method. Function probes capture invocation time, input parameters, and return values of target methods at runtime, acting as a “microscope” for pinpointing issues at the function level—complementing request-level latency analysis with per-call visibility.

Production safety net. “Works on staging, crashes in production” is every team’s nightmare, but you can’t arbitrarily restart or add debugging code during peak hours. Non-invasive probes attach and detach dynamically—an on-demand approach that adds insight without adding risk.

Security and compliance. For functions involving encryption, authentication, or financial transactions, teams need to monitor invocation patterns in real time. Probes provide deep call-trace visibility without exposing core source-code logic, supplying first-hand data for auditing and security review.

Complex-environment debugging. IDEs and breakpoints break down in containerized, distributed, cloud-native environments. Function probes offer a lighter approach that still captures the most critical function-level information where traditional debugging is impractical.

Hands-on Demo: Capturing Java Method Arguments in Production

Let’s demonstrate how to use function probes with a concrete example. Suppose we have the following Java method defined:

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

This method accepts three parameters: a string name, an integer age, and an object user. Our goal is to monitor the values of these parameters without modifying the code.

Step One: Obtain Function Entry Address

First, we need to use ylang to retrieve the entry address of the target method:

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

Provide the class name, method name, and method signature, respectively.

This command will return the entry address of the greetUser method, which is crucial information for setting up the probe.

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

Step Two: Setting up the Function Probe

Next, we use the acquired JIT method entry address as the watchpoint address to configure a method call interceptor:

#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();
}

The primary purpose of this probe code is to automatically capture parameter information when a Java method is invoked, thereby assisting developers in analyzing function behavior without interrupting services. Specifically, its execution flow can be broken down into the following key steps:

  • _watchpoint(ENTRY).exec: Sets a watchpoint at the target method’s entry. Once the method is called, the probe logic will be triggered.

  • nmethod_oop_arg(2), nmethod_int_arg(3), nmethod_oop_arg(4): These retrieve the 2nd, 3rd, and 4th parameters of the invoked method.

  • It’s important to note that in Java instance methods, the 1st parameter defaults to the this object. Therefore, the actual business-relevant parameters begin at index 2.

  • get_java_string(): Used to read a string object from the Java runtime as a printable string value.

  • nmethod_int_arg(): Used to extract integer-type parameter values.

  • dump_field_object(user_obj, "email"): Extracts the object corresponding to the email field from the User object.

  • get_java_string(email_obj): Converts the extracted email field object into a string for logging or subsequent analysis.

Through this probe logic, we can capture Java method parameter values in real-time without modifying the source code, and further extract key fields within objects. This provides significant observability for troubleshooting and behavior analysis.

Execution Results

Upon execution of the target method, the ylang executor will output the following monitoring data:

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

How OpenResty XRay Differs from Common Java Profilers

You just watched XRay attach to a live JVM and read per-call argument values with a few lines of ylang—no -javaagent loaded, no bytecode rewritten, no process restart. Here’s how that approach compares to other non-invasive Java profilers—Arthas, BTrace, JMC Agent, async-profiler, and JFR—which also avoid source-code changes:

  • No -javaagent, no bytecode instrumentation. Arthas and BTrace load a -javaagent and inject bytecode; XRay reads live JVM state from the outside.
  • Captures per-call argument values. async-profiler and JFR sample stacks—they show hot methods, not the values passed in. XRay captures the actual arguments and object fields on each call.
  • Works on both interpreted and JIT-compiled methods, with dynamic enable/disable at runtime.

One caveat: inline functions can’t be probed because they lack independent function entry points.

Frequently Asked Questions

Do I need to add a -javaagent to use OpenResty XRay?

No. OpenResty XRay reads live JVM state from outside the process. There is no -javaagent flag to add, no bootstrap JAR to preload, and no restart required to enable or disable probes.

Does this require bytecode instrumentation?

No. XRay does not modify or rewrite JVM bytecode. Method probes are set at the entry addresses of interpreted and JIT-compiled methods, so the running program’s bytecode remains untouched.

How is this different from Arthas’s watch command?

Arthas loads a -javaagent into the target process and uses bytecode enhancement to intercept methods. OpenResty XRay observes the JVM from outside without loading a -javaagent or modifying bytecode, and captures per-call argument values on both interpreted and JIT-compiled methods.

Can it capture parameter values for JIT-compiled methods?

Yes. As shown in the demo above, find_method_entry returns entry addresses for both interpreted and compiled (JIT) versions of a method, and the _watchpoint(...).exec probe fires on either path—capturing the actual argument values passed into each call.

What is OpenResty XRay

OpenResty XRay is a dynamic-tracing product that automatically analyzes your running applications to troubleshoot performance problems, behavioral issues, and security vulnerabilities with actionable suggestions. Under the hood, OpenResty XRay is powered by our Y language targeting various runtimes like Stap+, eBPF+, GDB, and ODB, depending on the contexts.

If you like this tutorial, please subscribe to this blog site and/or our YouTube channel. Thank you!

About The Author

Yichun Zhang (Github handle: agentzh), is the original creator of the OpenResty® open-source project and the CEO of OpenResty Inc..

Yichun is one of the earliest advocates and leaders of “open-source technology”. He worked at many internationally renowned tech companies, such as Cloudflare, Yahoo!. He is a pioneer of “edge computing”, “dynamic tracing” and “machine coding”, with over 22 years of programming and 16 years of open source experience. Yichun is well-known in the open-source space as the project leader of OpenResty®, adopted by more than 40 million global website domains.

OpenResty Inc., the enterprise software start-up founded by Yichun in 2017, has customers from some of the biggest companies in the world. Its flagship product, OpenResty XRay, is a non-invasive profiling and troubleshooting tool that significantly enhances and utilizes dynamic tracing technology. And its OpenResty Edge product is a powerful distributed traffic management and private CDN software product.

As an avid open-source contributor, Yichun has contributed more than a million lines of code to numerous open-source projects, including Linux kernel, Nginx, LuaJIT, GDB, SystemTap, LLVM, Perl, etc. He has also authored more than 60 open-source software libraries.