Skip to content

[Native Image][Tracing Agent] Reflective Field.get/set is over 100x slower on GraalVM 25 #13975

Description

@rotilho

Describe the Issue

The Native Image tracing agent in Oracle GraalVM 25 introduces severe runtime overhead for reflective field access when the default track-reflection-metadata=true behavior is enabled. Reflection-heavy applications and integration-test suites that complete normally on GraalVM 21 and 24 can take several times longer on GraalVM 25, eventually exceeding test-level or CI time limits.

This is particularly visible in Spring and Kotlin workloads, where serializers, dependency injection, binders, proxies, and test infrastructure repeatedly access fields through reflection. In an existing full application test matrix, the same agent-instrumented suite took:

  • Oracle GraalVM 21: 9m12s
  • Oracle GraalVM 24: 8m53s
  • Oracle GraalVM 25: 35m56s

The underlying per-operation regression is much larger than the full-suite ratio. With GraalVM 25.0.3, repeated Field.get calls take approximately 22 ms without the agent and 2,540 ms with the default agent: a 113x slowdown. The same operation on GraalVM 24 has almost no additional agent overhead.

The regression appears to align with #11839 / commit dc5c3a8a32fc0349d61803088de6626f7917da34, backported to 25.0 in #11894. That change moved reflection field registration from query time to access time and added JVMTI breakpoints to the Field.get*/set* methods. Consequently, every reflective field read or write appears to enter the breakpoint/JNI/trace path, including repeated accesses to a field that has already been observed.

There is currently no complete configuration workaround. Disabling track-reflection-metadata restores performance, but GraalVM 25 then omits the accessed field from reachability metadata. A native image built with that output succeeds at build time but fails at runtime with MissingReflectionRegistrationError for the omitted field.

A minimal dependency-free probe is provided below only to isolate and measure the problem. Three-run medians for 100,000 reflective field reads show that the regression starts with GraalVM 25 and remains present in both the latest tested 25.0 maintenance release and the tested 25.1 Innovation release:

Oracle GraalVM No agent Default agent Agent / no-agent Agent with track-reflection-metadata=false
21.0.11 39.109 ms 46.986 ms 1.20x 40.954 ms
24.0.2 36.983 ms 37.682 ms 1.02x 40.390 ms
25.0.1 23.166 ms 2,573.840 ms 111.10x 25.853 ms
25.0.3 22.406 ms 2,539.775 ms 113.35x 25.219 ms
25.1.3 23.411 ms 2,777.517 ms 118.64x 28.965 ms

Using the latest version of GraalVM can resolve many issues.

GraalVM Version

Oracle GraalVM 24 control:

java version "24.0.2" 2025-07-15
Java(TM) SE Runtime Environment Oracle GraalVM 24.0.2+11.1 (build 24.0.2+11-jvmci-b01)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 24.0.2+11.1 (build 24.0.2+11-jvmci-b01, mixed mode, sharing)

native-image 24.0.2 2025-07-15
GraalVM Runtime Environment Oracle GraalVM 24.0.2+11.1 (build 24.0.2+11-jvmci-b01)
Substrate VM Oracle GraalVM 24.0.2+11.1 (build 24.0.2+11, serial gc, compressed references)

Oracle GraalVM 25 LTS:

java version "25.0.3" 2026-04-21 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 25.0.3+9.1 (build 25.0.3+9-LTS-jvmci-b01)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 25.0.3+9.1 (build 25.0.3+9-LTS-jvmci-b01, mixed mode, sharing)

native-image 25.0.3 2026-04-21
GraalVM Runtime Environment Oracle GraalVM 25.0.3+9.1 (build 25.0.3+9-LTS-jvmci-b01)
Substrate VM Oracle GraalVM 25.0.3+9.1 (build 25.0.3+9-LTS, serial gc, compressed references)

Latest tested Oracle GraalVM 25.1 Innovation release:

java version "25.0.3" 2026-04-21 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 25.1.3+9.1 (build 25.0.3+9-LTS-jvmci-25.1-b19)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 25.1.3+9.1 (build 25.0.3+9-LTS-jvmci-25.1-b19, mixed mode, sharing)

native-image 25.0.3 2026-04-21
GraalVM Runtime Environment Oracle GraalVM 25.1.3+9.1 (build 25.0.3+9-LTS-jvmci-25.1-b19)
Substrate VM Oracle GraalVM 25.1.3+9.1 (build 25.0.3+9-LTS, serial gc, compressed references)

Operating System and Version

Linux pod-766978bc57-52vn6 6.18.36-talos #1 SMP PREEMPT_DYNAMIC Fri Jun 19 18:12:05 UTC 2026 x86_64 GNU/Linux Debian GNU/Linux 13 (trixie)

Build Command

Set JAVA_HOME to the Oracle GraalVM release under test, then run:

rm -rf classes agent agent-no-track R.java && mkdir classes && printf '%s\n' 'class R{int f;static volatile Object x;public static void main(String[]a)throws Exception{R o=new R();var f=Class.forName(a[0]).getDeclaredField(a[1]);int n=Integer.parseInt(a[2]);long t=System.nanoTime();for(int i=0;i<n;i++)x=f.get(o);System.out.printf("%s reflective Field.get calls: %.3f s%n",a[2],(System.nanoTime()-t)/1e9);}}' > R.java && "$JAVA_HOME/bin/javac" -d classes R.java && "$JAVA_HOME/bin/java" -cp classes R R f 100000 && "$JAVA_HOME/bin/java" -agentlib:native-image-agent=config-output-dir=agent -cp classes R R f 100000 && "$JAVA_HOME/bin/java" -agentlib:native-image-agent=config-output-dir=agent-no-track,track-reflection-metadata=false -cp classes R R f 100000

The complete application is one physical source line and has no external dependencies:

class R{int f;static volatile Object x;public static void main(String[]a)throws Exception{R o=new R();var f=Class.forName(a[0]).getDeclaredField(a[1]);int n=Integer.parseInt(a[2]);long t=System.nanoTime();for(int i=0;i<n;i++)x=f.get(o);System.out.printf("%s reflective Field.get calls: %.3f s%n",a[2],(System.nanoTime()-t)/1e9);}}

For a direct GraalVM 24 versus 25 comparison, set GRAALVM24_HOME and GRAALVM25_HOME, then run this literal shell one-liner:

d=$(mktemp -d); printf '%s\n' 'class R{int f;static volatile Object x;public static void main(String[]a)throws Exception{R o=new R();var f=Class.forName(a[0]).getDeclaredField(a[1]);int n=Integer.parseInt(a[2]);long t=System.nanoTime();for(int i=0;i<n;i++)x=f.get(o);System.out.printf("%s reflective Field.get calls: %.3f s%n",a[2],(System.nanoTime()-t)/1e9);}}' >"$d/R.java"; "$GRAALVM24_HOME/bin/javac" -d "$d" "$d/R.java"; for h in "$GRAALVM24_HOME" "$GRAALVM25_HOME"; do o=$(mktemp -d); "$h/bin/java" -version 2>&1 | sed -n '1p'; "$h/bin/java" "-agentlib:native-image-agent=config-output-dir=$o" -cp "$d" R R f 100000; done

Expected Behavior

Enabling the Native Image tracing agent is expected to add some overhead, but repeated reflective field access should not become over 100 times slower when moving from GraalVM 24 to GraalVM 25.

The default agent should record the dynamically accessed field:

{
  "reflection": [
    {
      "type": "R",
      "fields": [
        { "name": "f" }
      ]
    }
  ]
}

The runtime cost should remain reasonably close to GraalVM 24 or otherwise avoid processing every repeated access through the full breakpoint/JNI/trace path.

Actual Behavior

The exact direct-comparison shell command produced:

java version "24.0.2" 2025-07-15
100000 reflective Field.get calls: 0.040 s
java version "25.0.3" 2026-04-21 LTS
100000 reflective Field.get calls: 2.624 s

In the repeated benchmark, GraalVM 25.0.3 spent approximately 25,398 ns per reflective field read with the default agent, compared with approximately 224 ns without the agent.

track-reflection-metadata=false restores performance but changes the GraalVM 25 output to a bare type entry without R.f:

{
  "reflection": [
    {
      "type": "R"
    }
  ]
}

A native image built with that incomplete metadata exits with MissingReflectionRegistrationError for R.f.

Steps to Reproduce

  1. Install or select Oracle GraalVM 24.0.2 and Oracle GraalVM 25.0.3 or 25.1.3.
  2. Create the one-line R.java shown above.
  3. Compile it once with javac.
  4. Run R R f 100000 without the Native Image agent and record the elapsed time.
  5. Run the same class and arguments with -agentlib:native-image-agent=config-output-dir=agent and record the elapsed time.
  6. Repeat steps 3-5 unchanged on GraalVM 24 and GraalVM 25.
  7. Observe that GraalVM 24 completes the agent-instrumented loop in roughly 40 ms, while GraalVM 25 requires approximately 2.5-2.9 seconds.
  8. Optionally repeat with track-reflection-metadata=false and observe that performance returns to normal but agent-no-track/reachability-metadata.json does not contain field R.f.
  9. Build a native image using the no-tracking metadata and run ./r R f 1; observe MissingReflectionRegistrationError for R.f.

Additional Context

This is a focused reduction of the GraalVM 25 slowdown previously discussed in #10352.

The regression appears to align with the following change:

The description of #11839 states:

Registering fields for reflection was done at query time, so this PR changes it to be detected at access time (in Field.get/set) and adds the necessary agent breakpoints.

That commit added breakpoint handlers for all object and primitive Field.get*/set* variants. For each access, accessFieldInternal obtains the caller, declaring class, and field name through JNI and records an accessField trace. Because track-reflection-metadata defaults to true, those breakpoints are enabled in a normal agent run.

The performance/completeness trade-off was also verified through native execution on GraalVM 25.0.3:

Metadata mode Native build Native run
Default tracking exit 0 exit 0
track-reflection-metadata=false exit 0 exit 1: MissingReflectionRegistrationError for R.f

The impact is visible in larger reflection-heavy test suites:

Possibly related ongoing work:

A fast path, safe deduplication of repeated field accesses, or the planned non-breakpoint tracer would avoid paying the full breakpoint/JNI/trace cost on every reflective field read or write.

Build Log Output and Error Messages

Default tracking records R.f; a native image built with that metadata runs successfully:

1 reflective Field.get calls: 0.000 s

With track-reflection-metadata=false, the native image builds but fails at runtime:

Exception in thread "main" org.graalvm.nativeimage.MissingReflectionRegistrationError: Cannot reflectively read or write field 'int R.f'. To allow this operation, add the following to the 'reflection' section of 'reachability-metadata.json' and rebuild the native image:

  {
    "type": "R",
    "fields": [
      {
        "name": "f"
      }
    ]
  }

    at org.graalvm.nativeimage.builder/com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.reportAccessedField(MissingReflectionRegistrationUtils.java:79)
    at org.graalvm.nativeimage.builder/com.oracle.svm.core.reflect.UnsafeFieldUtil.getFieldOffset(UnsafeFieldUtil.java:46)
    at java.base@25.0.3/jdk.internal.misc.Unsafe.objectFieldOffset(Unsafe.java:44)
    at java.base@25.0.3/java.lang.reflect.Field.get(Field.java:438)
    at R.main(R.java:1)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions