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
- Install or select Oracle GraalVM 24.0.2 and Oracle GraalVM 25.0.3 or 25.1.3.
- Create the one-line
R.java shown above.
- Compile it once with
javac.
- Run
R R f 100000 without the Native Image agent and record the elapsed time.
- Run the same class and arguments with
-agentlib:native-image-agent=config-output-dir=agent and record the elapsed time.
- Repeat steps 3-5 unchanged on GraalVM 24 and GraalVM 25.
- Observe that GraalVM 24 completes the agent-instrumented loop in roughly 40 ms, while GraalVM 25 requires approximately 2.5-2.9 seconds.
- 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.
- 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)
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=truebehavior 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:
The underlying per-operation regression is much larger than the full-suite ratio. With GraalVM 25.0.3, repeated
Field.getcalls 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 theField.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-metadatarestores 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 withMissingReflectionRegistrationErrorfor 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:
track-reflection-metadata=falseUsing the latest version of GraalVM can resolve many issues.
GraalVM Version
Oracle GraalVM 24 control:
Oracle GraalVM 25 LTS:
Latest tested Oracle GraalVM 25.1 Innovation release:
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_HOMEto the Oracle GraalVM release under test, then run:The complete application is one physical source line and has no external dependencies:
For a direct GraalVM 24 versus 25 comparison, set
GRAALVM24_HOMEandGRAALVM25_HOME, then run this literal shell one-liner: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:
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=falserestores performance but changes the GraalVM 25 output to a bare type entry withoutR.f:{ "reflection": [ { "type": "R" } ] }A native image built with that incomplete metadata exits with
MissingReflectionRegistrationErrorforR.f.Steps to Reproduce
R.javashown above.javac.R R f 100000without the Native Image agent and record the elapsed time.-agentlib:native-image-agent=config-output-dir=agentand record the elapsed time.track-reflection-metadata=falseand observe that performance returns to normal butagent-no-track/reachability-metadata.jsondoes not contain fieldR.f../r R f 1; observeMissingReflectionRegistrationErrorforR.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:
[GR-67746] Base reflection TCK testsdc5c3a8a32fc0349d61803088de6626f7917da34The description of #11839 states:
That commit added breakpoint handlers for all object and primitive
Field.get*/set*variants. For each access,accessFieldInternalobtains the caller, declaring class, and field name through JNI and records anaccessFieldtrace. Becausetrack-reflection-metadatadefaults totrue, 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:
track-reflection-metadata=falseMissingReflectionRegistrationErrorforR.fThe impact is visible in larger reflection-heavy test suites:
https://github.com/rotilho/node-reproducer/actions/runs/19893636797
https://github.com/rotilho/spring-petclinic/actions/runs/19873594878
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:With
track-reflection-metadata=false, the native image builds but fails at runtime: