From b7fbbd16e386040d45768eeb0e2cad3dcb6bb0e1 Mon Sep 17 00:00:00 2001
From: Dominik Lano
Date: Thu, 20 Aug 2026 20:00:04 +0200
Subject: [PATCH] Verify all objects reachable through build-time-initialized
static fields
---
.../compiler/libgraal/LibGraalFeature.java | 5 +-
.../native-image/ClassInitialization.md | 12 +
.../org.graalvm.nativeimage/snapshot.sigtest | 10 +-
.../graalvm/nativeimage/hosted/Feature.java | 41 +-
.../hosted/FieldValueTransformer.java | 11 +-
...FieldValueTransformerRequiresUniverse.java | 67 +
.../clinit/ClassInitializationTracking.java | 6 +-
substratevm/CHANGELOG.md | 2 +
.../mx_substratevm_benchmark.py | 16 +-
.../pointsto/standalone/StandaloneHost.java | 4 +
.../StandaloneAnalysisFeatureImpl.java | 6 +-
...andaloneFieldValueAvailabilitySupport.java | 2 +-
.../StandaloneConstantReflectionProvider.java | 2 +-
.../com/oracle/graal/pointsto/api/HostVM.java | 8 +
.../pointsto/heap/HostedValuesProvider.java | 4 +-
.../graal/pointsto/meta/AnalysisField.java | 21 +-
.../graal/pointsto/meta/AnalysisType.java | 15 +-
.../graal/pointsto/meta/AnalysisUniverse.java | 73 +-
.../objectfile/debugentry/ClassEntry.java | 8 +-
...internal_foreign_abi_NativeEntryPoint.java | 5 +-
.../svm/core/FutureDefaultsOptions.java | 14 +-
.../svm/core/doc-files/FutureDefaultsHelp.txt | 4 +-
.../svm/core/feature/JVMCIFeatureAccess.java | 19 +-
.../FieldOffsetFieldValueTransformer.java | 6 +-
.../StaticFieldBaseFieldValueTransformer.java | 6 +-
.../src/com/oracle/svm/core/heap/GCCause.java | 14 +-
.../svm/core/jdk/ServiceCatalogSupport.java | 37 +-
...get_jdk_internal_module_ModuleReaders.java | 2 +-
.../oracle/svm/core/jdk/VarHandleSupport.java | 5 +-
.../svm/core/locks/ClassInstanceReplacer.java | 47 +-
.../oracle/svm/core/locks/VMLockSupport.java | 16 +-
.../target/ExecutableAccessorComputer.java | 6 +-
.../target/ReflectionFieldValueComputers.java | 8 +-
.../graal/hosted/FieldsOffsetsFeature.java | 7 +-
.../GraalGraphObjectReplacer.java | 77 +-
.../RuntimeCompilationFeature.java | 2 +-
.../Target_java_lang_reflect_Field.java | 2 +-
.../svm/guest/hosted/GuestFeatureHandler.java | 5 +
.../svm/guest/hosted/GuestFeatureImpl.java | 5 +
.../test/VerifyJavaTypeGetNameUsage.java | 3 +-
.../hosted/test/VerifyReflectionUsage.java | 1 +
.../oracle/svm/hosted/ClassLoaderFeature.java | 33 +-
.../com/oracle/svm/hosted/FeatureImpl.java | 73 +-
.../hosted/GuestFeatureDispatchFeature.java | 18 +
.../svm/hosted/HeapObjectReplacerAdapter.java | 57 +
.../oracle/svm/hosted/ModuleLayerFeature.java | 25 +-
.../svm/hosted/NativeImageGenerator.java | 11 +
.../src/com/oracle/svm/hosted/SVMHost.java | 31 +-
.../AnalysisConstantReflectionProvider.java | 7 +
.../hosted/ameta/CustomTypeFieldHandler.java | 8 +-
.../ameta/FieldValueInterceptionSupport.java | 251 ++-
.../hosted/ameta/HostedDynamicHubFeature.java | 30 +-
.../hosted/ameta/SVMHostedValueProvider.java | 13 +-
.../svm/hosted/c/CGlobalDataFeature.java | 31 +-
.../ClassInitializationSupport.java | 25 +-
.../hosted/classinitialization/InitKind.java | 4 +-
...edictableBuildTimeInitializationError.java | 299 +++
...ictableBuildTimeInitializationFeature.java | 1950 +++++++++++++++++
...edictableBuildTimeInitializationField.java | 173 ++
...bleBuildTimeInitializationLookupLayer.java | 232 ++
...redictableBuildTimeInitializationType.java | 500 +++++
.../code/CEntryPointLiteralFeature.java | 12 +-
.../heap/ImageHeapCollectionFeature.java | 5 +-
.../heap/ImageHeapCollectionSupport.java | 57 +-
.../svm/hosted/image/sources/SourceCache.java | 15 +-
.../HostedClassLoaderPackageManagement.java | 10 +-
.../hosted/jdk/JDKInitializationFeature.java | 4 +
.../svm/hosted/jfr/JfrEventFeature.java | 67 +-
.../svm/hosted/jfr/JfrEventSubstitution.java | 77 +-
.../AutomaticUnsafeTransformationSupport.java | 107 +-
.../substitute/FieldValueTransformation.java | 13 +-
.../thread/HostedJavaThreadsFeature.java | 78 +-
.../thread/LayeredVMThreadLocalCollector.java | 15 +-
.../hosted/thread/VMThreadLocalCollector.java | 8 +-
.../option/FutureDefaultsOptionsSupport.java | 2 +
.../ImageSingletonsSupportImpl.java | 10 +-
.../truffle/api/SubstrateTruffleRuntime.java | 8 +-
.../util/HeapConstantToConstantReplacer.java | 67 +
.../svm/util/HeapObjectConstantReplacer.java | 124 ++
.../oracle/svm/util/HeapObjectReplacer.java | 77 +
.../com/oracle/svm/util/InterceptedField.java | 49 +
.../oracle/svm/util/InterceptedFieldType.java | 56 +
.../system/WebImageJavaLangSubstitutions.java | 5 +
83 files changed, 4898 insertions(+), 343 deletions(-)
create mode 100644 sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/HeapObjectReplacerAdapter.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationError.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationFeature.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java
create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java
create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java
create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java
create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java
create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java
create mode 100644 substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java
diff --git a/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java b/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java
index 212b96c37c694..673acaf7fc5b4 100644
--- a/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java
+++ b/compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/LibGraalFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -52,6 +52,7 @@
import org.graalvm.nativeimage.Platforms;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.FieldValueTransformer;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import org.graalvm.nativeimage.libgraal.hosted.LibGraalLoader;
@@ -261,7 +262,7 @@ void afterAnalysis(AfterAnalysisAccess access) {
* Transformer for {@code Fields.offsets} and {@code Edges.iterationMask} which need to be
* recomputed to use SVM field offsets instead of HotSpot field offsets.
*/
- class FieldOffsetsTransformer implements FieldValueTransformer {
+ class FieldOffsetsTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse {
/**
* Map from {@link Fields} objects to a (newOffsets, newIterationMask) tuple represented as
* a {@link java.util.Map.Entry} value.
diff --git a/docs/reference-manual/native-image/ClassInitialization.md b/docs/reference-manual/native-image/ClassInitialization.md
index 5288a276d1b97..9f60e0dc5a6ca 100644
--- a/docs/reference-manual/native-image/ClassInitialization.md
+++ b/docs/reference-manual/native-image/ClassInitialization.md
@@ -26,6 +26,8 @@ To accommodate this requirement, a short-term "relevant supertype" is used, as w
* Relevant supertypes of types initialized at build time must also be initialized at build time.
* Relevant subtypes of types initialized at runtime must also be initialized at runtime.
* No instances of classes that are initialized at runtime must be present in the executable.
+* With `--future-defaults=predictable-build-time-initialization`, a class initialized at build time must not reference instances of run-time initialized classes, directly or indirectly, through its static fields,
+ even if reachability analysis does not mark those instances as reachable.
To enjoy the complete out-of-the-box experience of Native Image and still get the benefits of build-time initialization, Native Image does two things:
* [Build-Time Initialization](#build-time-initialization)
@@ -41,6 +43,16 @@ Native Image initializes most JDK classes at build time, including the garbage c
For all of the classes that are initialized at build time, Native Image gives proper support so that the semantics remain consistent despite class initialization occurring at build time.
If you discover an issue with a JDK class behaving incorrectly because of class initialization at build time, please [report an issue](https://github.com/oracle/graal/issues/new).
+### Verification of Objects Referenced by Static Fields
+
+By default, Native Image verifies that objects included in the executable's image heap are instances of build-time initialized classes and are allowed in the image heap.
+
+With `--future-defaults=predictable-build-time-initialization`, this verification extends to any object referenced directly
+or indirectly by static fields of build-time initialized classes that could become part of the image heap.
+This makes reporting of class initialization violations more reliable, as these errors are no longer masked by reachability.
+If an object is found to be an instance of a run-time initialized class, the build fails with an error showing a path from a
+build-time initialized class to the offending object and suggesting how to resolve the violation.
+
## Automatic Initialization of Safe Classes
For application classes, Native Image tries to find classes that can be safely initialized at build time.
diff --git a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest
index 647fa0ec0d4c3..c10b27396d10d 100644
--- a/sdk/src/org.graalvm.nativeimage/snapshot.sigtest
+++ b/sdk/src/org.graalvm.nativeimage/snapshot.sigtest
@@ -996,6 +996,7 @@ innr public abstract interface static AfterImageWriteAccess
innr public abstract interface static AfterRegistrationAccess
innr public abstract interface static BeforeAnalysisAccess
innr public abstract interface static BeforeCompilationAccess
+innr public abstract interface static BeforeFieldValueAccess
innr public abstract interface static BeforeHeapLayoutAccess
innr public abstract interface static BeforeImageWriteAccess
innr public abstract interface static BeforeUniverseBuildingAccess
@@ -1054,6 +1055,7 @@ meth public abstract org.graalvm.nativeimage.dynamicaccess.ResourceAccess getRes
CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess
outer org.graalvm.nativeimage.hosted.Feature
+intf org.graalvm.nativeimage.hosted.Feature$BeforeFieldValueAccess
intf org.graalvm.nativeimage.hosted.Feature$FeatureAccess
meth public abstract !varargs void registerReachabilityHandler(java.util.function.Consumer,java.lang.Object[])
meth public abstract void registerAsAccessed(java.lang.reflect.Field)
@@ -1061,8 +1063,8 @@ meth public abstract void registerAsInHeap(java.lang.Class>)
meth public abstract void registerAsUnsafeAccessed(java.lang.reflect.Field)
meth public abstract void registerAsUnsafeAllocated(java.lang.Class>)
meth public abstract void registerAsUsed(java.lang.Class>)
+meth public abstract void registerBeforeFieldValueAccessHandler(java.util.function.BiConsumer>,java.lang.Class>)
meth public abstract void registerClassInitializerReachabilityHandler(java.util.function.Consumer,java.lang.Class>)
-meth public abstract void registerFieldValueTransformer(java.lang.reflect.Field,org.graalvm.nativeimage.hosted.FieldValueTransformer)
meth public abstract void registerMethodOverrideReachabilityHandler(java.util.function.BiConsumer,java.lang.reflect.Executable)
meth public abstract void registerSubtypeReachabilityHandler(java.util.function.BiConsumer>,java.lang.Class>)
@@ -1070,6 +1072,10 @@ CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$Bef
outer org.graalvm.nativeimage.hosted.Feature
intf org.graalvm.nativeimage.hosted.Feature$CompilationAccess
+CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeFieldValueAccess
+ outer org.graalvm.nativeimage.hosted.Feature
+meth public abstract void registerFieldValueTransformer(java.lang.reflect.Field,org.graalvm.nativeimage.hosted.FieldValueTransformer)
+
CLSS public abstract interface static org.graalvm.nativeimage.hosted.Feature$BeforeHeapLayoutAccess
outer org.graalvm.nativeimage.hosted.Feature
intf org.graalvm.nativeimage.hosted.Feature$CompilationAccess
@@ -1135,6 +1141,8 @@ CLSS public abstract interface org.graalvm.nativeimage.hosted.FieldValueTransfor
meth public abstract java.lang.Object transform(java.lang.Object,java.lang.Object)
meth public boolean isAvailable()
+CLSS public abstract interface org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse
+
CLSS public final org.graalvm.nativeimage.hosted.RuntimeClassInitialization
meth public !varargs static void initializeAtBuildTime(java.lang.Class>[])
meth public !varargs static void initializeAtBuildTime(java.lang.String[])
diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java
index 6aa6a9e6cf139..1a6db312afaec 100644
--- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java
+++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/Feature.java
@@ -267,13 +267,36 @@ interface DuringSetupAccess extends FeatureAccess {
void registerBuildTimeBootstrapCondy(Executable method);
}
+ /**
+ * Interface that encapsulates registration of field value transformers. Under
+ * {@code --future-defaults=predictable-build-time-initialization}, registration of transformers
+ * will only be possible before analysis starts or in a
+ * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler},
+ * which is triggered before the first field of the selected class is accessed.
+ */
+ @Platforms(Platform.HOSTED_ONLY.class)
+ interface BeforeFieldValueAccess {
+ /**
+ * Registers a field value transformer for the provided field.
+ * Under {@code --future-defaults=predictable-build-time-initialization}, registration of
+ * transformers will only be possible before analysis starts or in a
+ * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}.
+ *
+ *
+ * See the JavaDoc of
+ * {@link FieldValueTransformer} for details.
+ *
+ */
+ void registerFieldValueTransformer(Field field, FieldValueTransformer transformer);
+ }
+
/**
* Access methods available for {@link Feature#beforeAnalysis}.
*
* @since 19.0
*/
@Platforms(Platform.HOSTED_ONLY.class)
- interface BeforeAnalysisAccess extends FeatureAccess {
+ interface BeforeAnalysisAccess extends FeatureAccess, BeforeFieldValueAccess {
/**
* Registers the provided type a used, i.e., metadata for the type is put into the native
@@ -380,12 +403,18 @@ interface BeforeAnalysisAccess extends FeatureAccess {
void registerClassInitializerReachabilityHandler(Consumer callback, Class> clazz);
/**
- * Registers a field value transformer for the provided field. See the JavaDoc of
- * {@link FieldValueTransformer} for details.
- *
- * @since 22.3
+ * Registers a callback that allows registration of field value transformers on demand. The
+ * callback is triggered for every subclass of {@code baseClass}, including
+ * {@code baseClass} itself, before the first field value of the class is accessed. The class
+ * is passed as the callback's second parameter.
+ *
+ *
+ * This is a replacement for registration done in type/subtype reachability handlers, where
+ * this behavior can no longer be supported. Handlers must be registered in a
+ * {@link Feature#beforeAnalysis beforeAnalysis} callback.
+ *
*/
- void registerFieldValueTransformer(Field field, FieldValueTransformer transformer);
+ void registerBeforeFieldValueAccessHandler(BiConsumer> callback, Class> baseClass);
}
/**
diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java
index 0996bfa460480..1cc4d2a73e3dd 100644
--- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java
+++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformer.java
@@ -44,9 +44,12 @@
import org.graalvm.nativeimage.Platforms;
import org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess;
+import java.util.function.BiConsumer;
+
/**
* A transformer for a field value that can be registered using
- * {@link BeforeAnalysisAccess#registerFieldValueTransformer}.
+ * {@link BeforeAnalysisAccess#registerFieldValueTransformer} or in a
+ * {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}.
*
* At image build time, the field value transformer provides the value of the field for the image
* heap. Without a transformer, the value of the field in the image heap is the same as the hosted
@@ -70,6 +73,12 @@
* reachability handler} for the declaring class of the field, or a
* {@link BeforeAnalysisAccess#registerSubtypeReachabilityHandler subtype reachability handler} for
* a super-type of the declaring class of the field.
+ *
+ * Under {@code --future-defaults=predictable-build-time-initialization},
+ * registration of transformers will only be possible before static analysis starts
+ * or in a {@link BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class) before-field-value-access handler}.
+ * The handler is triggered for each subclass, including the base class, before its first field
+ * value is accessed.
*
* @since 22.3
*/
diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java
new file mode 100644
index 0000000000000..bfe3a0a5e0a86
--- /dev/null
+++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/hosted/FieldValueTransformerRequiresUniverse.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or
+ * data (collectively the "Software"), free of charge and under any and all
+ * copyright rights in the Software, and any and all patent rights owned or
+ * freely licensable by each licensor hereunder covering either (i) the
+ * unmodified Software as contributed to or provided by such licensor, or (ii)
+ * the Larger Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ *
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ *
+ * The above copyright notice and either this complete permission notice or at a
+ * minimum a reference to the UPL must be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.graalvm.nativeimage.hosted;
+
+/**
+ * Marker interface to denote that a field value transformer
+ * requires the analysis/hosted universe or internal operations that expand the universe or image heap,
+ * such as creating new universe types, methods or fields, modifying existing ones,
+ * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList}
+ * or {@code ImageHeapMap}.
+ *
+ *
+ * For example, {@code com.oracle.svm.core.reflect.target.ExecutableAccessorComputer}
+ * may create an accessor for a method or a constructor, which requires the analysis.
+ *
+ *
+ *
+ * This marker interface is used by {@code PredictableBuildTimeInitializationFeature}
+ * to differentiate between fields that can be read outside the universe and
+ * those that cannot be read are processed only if the field is read and its receiver is reachable.
+ *
+ *
+ *
+ * This marker interface can be used on external code, but is mostly relevant for internal transformers.
+ * It is placed in this package in order to be available for {@code jdk.graal.compiler.libgraal.LibGraalFeature}.
+ *
+ */
+public interface FieldValueTransformerRequiresUniverse {
+}
diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java
index 79fe21dcc11a7..5ba194357af51 100644
--- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java
+++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/impl/clinit/ClassInitializationTracking.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
@@ -47,6 +47,10 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+@Platforms(Platform.HOSTED_ONLY.class)
public class ClassInitializationTracking {
public static final Map, StackTraceElement[]> initializedClasses = new ConcurrentHashMap<>();
diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md
index 5958f1a206823..ec124afe5d847 100644
--- a/substratevm/CHANGELOG.md
+++ b/substratevm/CHANGELOG.md
@@ -14,6 +14,8 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-77491) Bundle format version is now 2.0, with explicit path root kinds, build argument groups that retain their source platform, and persistent markers for unavailable inputs.
Older bundle formats remain readable.
* (GR-79366) Add auxiliary images: a mechanism for persisting runtime-allocated objects and runtime-compiled code, and loading them in later executions of the same native image.
+* (GR-51046) Introduced `--future-defaults=predictable-build-time-initialization`, which verifies that the classes of all objects stored in or reachable from static fields of build-time-initialized classes are also initialized at build time, even if those fields are not reachable. It also forbids registration of `FieldValueTransformer`s during analysis or in a reachability handler.
+* (GR-51046) Added `BeforeAnalysis#registerBeforeFieldValueAccessHandler`, which registers a handler that allows field value transformer registration on demand, before field values are accessed.
## GraalVM 25.4 (Internal Version 25.4.4)
* (GR-75824) When native executables are built with `-H:+StrictRuntimeJavaOptions`, runtime assertion options (for example, `-ea`, `-da`, `-esa`, and `-dsa`) are supported and configure the assertion status of runtime-loaded classes and runtime-initialized image classes. They do not affect build-time-initialized classes whose assertion status is *only* configured by `native-image -ea ...`.
diff --git a/substratevm/mx.substratevm/mx_substratevm_benchmark.py b/substratevm/mx.substratevm/mx_substratevm_benchmark.py
index 94d9174e824bb..2d6e8e198271a 100644
--- a/substratevm/mx.substratevm/mx_substratevm_benchmark.py
+++ b/substratevm/mx.substratevm/mx_substratevm_benchmark.py
@@ -726,11 +726,11 @@ def _empty_file():
_DACAPO_EXTRA_IMAGE_BUILD_ARGS = {
'h2' : [],
- 'pmd': [],
+ 'pmd': ['--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'],
# org.apache.crimson.parser.Parser2 is force initialized at build-time due to non-determinism in class initialization
# order that can lead to runtime issues. See GR-26324.
'xalan': ['--initialize-at-build-time=org.apache.crimson.parser.Parser2,org.apache.crimson.parser.Parser2$Catalog,org.apache.crimson.parser.Parser2$NullHandler,org.apache.xml.utils.res.CharArrayWrapper'],
- # There are two main issues with fop:
+ # There are three main issues with fop:
# 1. LoggingFeature is enabled by default, causing the LogManager configuration to be parsed at build-time. However
# DaCapo Harness sets the `java.util.logging.config.file` property at run-time. Therefore, we set
# `java.util.logging.config.file` to an empty file to avoid incorrectly parsing the default log configuration,
@@ -738,8 +738,10 @@ def _empty_file():
# not exist and would fail the benchmark when assertions are enabled.
# 2. Native-image picks a different service provider than the JVM for javax.xml.transform.TransformerFactory.
# We can simply remove the jar containing that provider as it is not required for the benchmark to run.
+ # 3. The Xerces DatatypeFactory provider is reachable from build-time initialized JAXB state.
'fop': [f"-Djava.util.logging.config.file={_empty_file()}",
- '--initialize-at-run-time=org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList'],
+ '--initialize-at-run-time=org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList',
+ '--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'],
'batik': []
}
@@ -952,6 +954,10 @@ def successPatterns(self):
_scala_daCapo_additional_lib = {
}
+_scala_daCapo_extra_image_build_args = {
+ 'scalac': ['--initialize-at-build-time=org.apache.xerces.jaxp.datatype.DatatypeFactoryImpl'],
+}
+
class ScalaDaCapoNativeImageBenchmarkSuite(mx_sdk_benchmark.ScalaDaCapoBenchmarkSuite, BaseDaCapoNativeImageBenchmarkSuite, mx_sdk_benchmark.NativeImageBenchmarkMixin): #pylint: disable=too-many-ancestors
def name(self):
@@ -996,6 +1002,10 @@ def skip_agent_assertions(self, benchmark, args):
else:
return []
+ def extra_image_build_argument(self, benchmark, args):
+ default_args = _scala_daCapo_extra_image_build_args.get(benchmark, [])
+ return default_args + super().extra_image_build_argument(benchmark, args)
+
def createCommandLineArgs(self, benchmarks, bmSuiteArgs):
if benchmarks is None:
mx.abort("Suite can only run a single benchmark per VM instance.")
diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java
index 91cf4b67f616d..d364c1042b87e 100644
--- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java
+++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/StandaloneHost.java
@@ -181,6 +181,10 @@ public Optional handleForeignCall(ForeignCallDescriptor foreignC
throw AnalysisError.shouldNotReachHere("StandaloneHost.handleForeignCall");
}
+ @Override
+ public void beforeFieldValueAccess(ResolvedJavaType type) {
+ }
+
@Override
public boolean isClosedTypeWorld() {
return closedTypeWorld;
diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java
index 2643265bf0ccb..f6629adb820ff 100644
--- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java
+++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/features/StandaloneAnalysisFeatureImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2022, Alibaba Group Holding Limited. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@@ -248,6 +248,10 @@ public void registerClassInitializerReachabilityHandler(Consumer> callback, Class> subclass) {
+ }
}
public static class DuringAnalysisAccessImpl extends BeforeAnalysisAccessImpl implements Feature.DuringAnalysisAccess {
diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java
index 2465c07c38fbd..a0cca49a8e65b 100644
--- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java
+++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/heap/StandaloneFieldValueAvailabilitySupport.java
@@ -117,7 +117,7 @@ private boolean isValueAvailable(AnalysisField field, Supplier raw
}
private boolean staticFieldSnapshottingAllowed(AnalysisField field) {
- field.beforeFieldValueAccess();
+ field.markDeclaringClassAsReachable();
if (!field.isStatic()) {
return true;
}
diff --git a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java
index e914ee9f0f85d..647c9c5cac075 100644
--- a/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java
+++ b/substratevm/src/com.oracle.graal.pointsto.standalone/src/com/oracle/graal/pointsto/standalone/meta/StandaloneConstantReflectionProvider.java
@@ -166,7 +166,7 @@ public JavaConstant readArrayElement(JavaConstant array, int index) {
public final JavaConstant readFieldValue(ResolvedJavaField f, JavaConstant receiver) {
AnalysisField field = (AnalysisField) f;
- field.beforeFieldValueAccess();
+ field.markDeclaringClassAsReachable();
if (field.equals(commonPoolField)) {
/*
diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java
index 39ac3616dc6ff..ce8bae3917f05 100644
--- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java
+++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/api/HostVM.java
@@ -171,6 +171,14 @@ public void registerType(AnalysisType newValue, int identityHashCode) {
public void checkType(ResolvedJavaType type, AnalysisUniverse universe) {
}
+ /**
+ * Register all field value transformers before fields of this type
+ * are accessed.
+ *
+ * @param type the hosted type whose fields are being accessed
+ */
+ public abstract void beforeFieldValueAccess(ResolvedJavaType type);
+
/**
* Run initialization tasks for a newly created {@link AnalysisType}.
*
diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java
index 1929873201b66..1df51f21383ab 100644
--- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java
+++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/heap/HostedValuesProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -51,7 +51,7 @@ public JavaConstant readFieldValueWithReplacement(AnalysisField field, JavaConst
}
private JavaConstant doReadValue(AnalysisField field, JavaConstant receiver) {
- field.beforeFieldValueAccess();
+ field.markDeclaringClassAsReachable();
return interceptHosted(GuestAccess.get().getProviders().getConstantReflection().readFieldValue(field.wrapped, receiver));
}
diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java
index f187ec75bc92e..93ecca4b4cb05 100644
--- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java
+++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisField.java
@@ -38,6 +38,7 @@
import com.oracle.svm.shared.meta.GuaranteeFolded;
import com.oracle.svm.util.GuestAccess;
import com.oracle.svm.util.GuestAnnotationAccess;
+import com.oracle.svm.util.InterceptedField;
import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.svm.util.OriginalFieldProvider;
@@ -48,7 +49,7 @@
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaType;
-public abstract class AnalysisField extends AnalysisElement implements WrappedJavaField, OriginalFieldProvider {
+public abstract class AnalysisField extends AnalysisElement implements WrappedJavaField, OriginalFieldProvider, InterceptedField {
static final AnalysisField[] EMPTY_ARRAY = new AnalysisField[0];
@@ -203,6 +204,7 @@ public int hashCode() {
return id;
}
+ @Override
public JavaKind getStorageKind() {
return fieldType.getStorageKind();
@@ -396,10 +398,12 @@ protected void onTrackedAcrossLayers(Object reason) {
AnalysisError.guarantee(!getUniverse().sealed(), "Field %s was marked as tracked after the universe was sealed", this);
}
+ @Override
public Object getFieldValueInterceptor() {
return fieldValueInterceptor;
}
+ @Override
public void setFieldValueInterceptor(Object fieldValueInterceptor) {
this.fieldValueInterceptor = fieldValueInterceptor;
}
@@ -476,11 +480,13 @@ public JavaConstant getConstantValue() {
}
/**
- * Ensure that all reachability handlers that were present at the time the declaring type was
- * marked as reachable are executed before accessing field values. This allows a field value
- * transformer to be installed reliably in a reachability handler.
+ * Mark the declaring class and its super types as reachable.
+ * This was the default behavior when reading a field, but
+ * registration of field value transformers in reachability
+ * handlers, under "--future-defaults=predictable-build-time-initialization"
+ * is forbidden to not affect the universe.
*/
- public void beforeFieldValueAccess() {
+ public void markDeclaringClassAsReachable() {
declaringClass.registerAsReachable(this);
declaringClass.forAllSuperTypes(type -> {
@@ -498,4 +504,9 @@ public void beforeFieldValueAccess() {
}
});
}
+
+ @Override
+ public void beforeFieldValueAccess() {
+ getUniverse().hostVM().beforeFieldValueAccess(declaringClass);
+ }
}
diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java
index ad1b396e25724..e25d6d4a34a25 100644
--- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java
+++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisType.java
@@ -60,6 +60,7 @@
import com.oracle.graal.pointsto.util.ConcurrentLightHashSet;
import com.oracle.svm.shared.util.LogUtils;
import com.oracle.svm.util.GuestAccess;
+import com.oracle.svm.util.InterceptedFieldType;
import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.svm.util.OriginalMethodProvider;
@@ -79,7 +80,7 @@
import jdk.vm.ci.meta.Signature;
import jdk.vm.ci.meta.UnresolvedJavaType;
-public abstract class AnalysisType extends AnalysisElement implements WrappedJavaType, OriginalClassProvider, Comparable {
+public abstract class AnalysisType extends AnalysisElement implements WrappedJavaType, OriginalClassProvider, Comparable, InterceptedFieldType {
private static final AtomicReferenceFieldUpdater UNSAFE_ACCESS_FIELDS_UPDATER = //
AtomicReferenceFieldUpdater.newUpdater(AnalysisType.class, Object.class, "unsafeAccessedFields");
@@ -912,21 +913,11 @@ public Object getReachableReason() {
* the field on the Java type system level). For example {@link WordBase word types} have a
* {@link #getJavaKind} of {@link JavaKind#Object}, but a primitive {@link #storageKind}.
*/
+ @Override
public final JavaKind getStorageKind() {
return storageKind;
}
- /**
- * Returns true if this type is part of the word type hierarchy, i.e, implements
- * {@link WordBase}.
- */
- public boolean isWordType() {
- /* Word types are currently the only types where kind and storageKind differ. */
- boolean wordType = getJavaKind() != getStorageKind();
- assert !wordType || getJavaKind().isObject() : Assertions.errorMessage("Only words are expected to have a discrepancy between java kind and storage kind", this);
- return wordType;
- }
-
@Override
public ResolvedJavaType getWrapped() {
return wrapped;
diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java
index 9aa6d75354ded..9d303eb1b7f6b 100644
--- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java
+++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/meta/AnalysisUniverse.java
@@ -61,6 +61,8 @@
import com.oracle.graal.pointsto.meta.AnalysisElement.MethodOverrideReachableNotification;
import com.oracle.graal.pointsto.util.AnalysisError;
import com.oracle.graal.pointsto.util.ConcurrentLightHashSet;
+import com.oracle.svm.util.HeapConstantToConstantReplacer;
+import com.oracle.svm.util.HeapObjectConstantReplacer;
import com.oracle.svm.util.OriginalClassProvider;
import jdk.graal.compiler.api.replacements.SnippetReflectionProvider;
@@ -712,13 +714,80 @@ private JavaConstant replaceConstant(JavaConstant source, boolean allowObjectToC
JavaConstant destination = source;
for (Function replacer : objectReplacers) {
- destination = Objects.requireNonNull(replacer.apply(destination));
+ if (replacer instanceof HeapObjectConstantReplacer heapObjectReplacer) {
+ destination = Objects.requireNonNull(heapObjectReplacer.replaceConstant(destination, true));
+ } else {
+ destination = Objects.requireNonNull(replacer.apply(destination));
+ }
+ assert !HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(destination);
assert destination.getJavaKind() == JavaKind.Object : destination;
}
+ return finishObjectReplacement(destination, allowObjectToConstantReplacement);
+ }
+
+ /**
+ * Applies registered replacers without using internal operations described in
+ * {@link com.oracle.svm.util.HeapObjectReplacer} that expand the analysis/hosted universe or image heap.
+ * This supports verification of objects that are not otherwise reachable by the image heap.
+ *
+ * @return the replaced constant, {@link HeapObjectConstantReplacer#CANNOT_REPLACE_MARKER}, or
+ * {@code source} if no replacer changes it
+ */
+ public JavaConstant replaceConstantNoHeap(JavaConstant source) {
+ assert !(source instanceof ImageHeapConstant) : "ImageHeapConstants were already processed and must not reach object replacement again: " + source;
+ if (source == null || source.getJavaKind() != JavaKind.Object || source.isNull()) {
+ return source;
+ }
+
+ JavaConstant destination = source;
+ for (Function replacer : objectReplacers) {
+ if (replacer instanceof HeapObjectConstantReplacer heapObjectReplacer) {
+ destination = Objects.requireNonNull(heapObjectReplacer.replaceConstant(destination, false));
+ if (HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(destination)) {
+ return destination;
+ }
+ } else {
+ destination = Objects.requireNonNull(replacer.apply(destination));
+ }
+ assert destination.getJavaKind() == JavaKind.Object : destination;
+ }
+
+ return finishObjectReplacement(destination, true, false);
+ }
+
+ private JavaConstant finishObjectReplacement(JavaConstant destination, boolean allowObjectToConstantReplacement) {
+ return finishObjectReplacement(destination, allowObjectToConstantReplacement, true);
+ }
+
+ /**
+ * Applies object-to-constant replacers.
+ *
+ *
+ * When {@code reachable} is {@code false}, then these replacers cannot
+ * use internal operations that expand the analysis/hosted universe or image heap,
+ * described in {@link HeapConstantToConstantReplacer}.
+ *
+ *
+ * @return Replacement constant, or {@link HeapConstantToConstantReplacer#CANNOT_REPLACE_MARKER},
+ * if the replacer could not replace the constant without using an internal operation
+ * that expands the analysis/hosted universe or image heap.
+ */
+ private JavaConstant finishObjectReplacement(JavaConstant destination, boolean allowObjectToConstantReplacement, boolean reachable) {
ImageHeapConstant imageHeapConstant = null;
for (Function replacer : objectToConstantSnapshotReplacers) {
- ImageHeapConstant result = replacer.apply(destination);
+ ImageHeapConstant result;
+ if (replacer instanceof HeapConstantToConstantReplacer> heapObjectReplacer) {
+ var heapResult = heapObjectReplacer.replaceConstant(destination, reachable);
+ if (HeapConstantToConstantReplacer.CANNOT_REPLACE_MARKER.equals(heapResult)) {
+ return heapResult;
+ }
+ AnalysisError.guarantee(heapResult == null || heapResult instanceof ImageHeapConstant,
+ "Heap object to constant replacer must always return null, ImageHeapConstant, or the marker constant: %s", heapResult);
+ result = (ImageHeapConstant) heapResult;
+ } else {
+ result = replacer.apply(destination);
+ }
if (result != null) {
AnalysisError.guarantee(allowObjectToConstantReplacement, "Object to constant replacement has been triggered from an unsupported location");
AnalysisError.guarantee(imageHeapConstant == null, "Multiple object to constant replacers have been triggered on a single object %s %s %s", destination, imageHeapConstant, result);
diff --git a/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java b/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java
index 51f4d44601c24..ba0bdd4f6eac9 100644
--- a/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java
+++ b/substratevm/src/com.oracle.objectfile/src/com/oracle/objectfile/debugentry/ClassEntry.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2020, Red Hat Inc. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@@ -102,7 +102,11 @@ public void seal() {
assert methods instanceof ArrayList && compiledMethods instanceof ArrayList &&
files instanceof ArrayList && indexedFiles == null && dirs instanceof ArrayList &&
indexedDirs == null : "ClassEntry should only be sealed once";
- methods = List.copyOf(methods);
+ methods = methods.stream()
+ .sorted(Comparator.comparing(MethodEntry::isDeopt)
+ .thenComparing(MethodEntry::getMethodName)
+ .thenComparing(MethodEntry::getSymbolName))
+ .toList();
methods.forEach(MethodEntry::seal);
compiledMethods = compiledMethods.stream().sorted(Comparator.comparing(CompiledMethodEntry::primary)).toList();
diff --git a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java
index edcb51c9013ca..face06a9c33f4 100644
--- a/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java
+++ b/substratevm/src/com.oracle.svm.core.foreign/src/com/oracle/svm/core/foreign/Target_jdk_internal_foreign_abi_NativeEntryPoint.java
@@ -31,6 +31,7 @@
import org.graalvm.nativeimage.Platforms;
import org.graalvm.nativeimage.c.function.CFunctionPointer;
import org.graalvm.nativeimage.hosted.FieldValueTransformer;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
import com.oracle.svm.core.annotate.Inject;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
@@ -128,7 +129,7 @@ public Object transform(Object receiver, Object originalValue) {
}
@Platforms(Platform.HOSTED_ONLY.class)
- static final class DowncallAddressTransformer implements FieldValueTransformer {
+ static final class DowncallAddressTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse {
private final ForeignFunctionsRuntime foreignFunctionsRuntime = ForeignFunctionsRuntime.singleton();
@Override
@@ -155,7 +156,7 @@ static UserException rethrowMissingForeignRegistrationError(MissingForeignRegist
}
@Platforms(Platform.HOSTED_ONLY.class)
- static final class DowncallInvokerAddressTransformer implements FieldValueTransformer {
+ static final class DowncallInvokerAddressTransformer implements FieldValueTransformer, FieldValueTransformerRequiresUniverse {
private final ForeignFunctionsRuntime foreignFunctionsRuntime = ForeignFunctionsRuntime.singleton();
@Override
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java
index e0e1541b4cdc6..f67e145a7731c 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/FutureDefaultsOptions.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -96,8 +96,9 @@ public class FutureDefaultsOptions {
private static final String CLASS_FOR_NAME_RESPECTS_CLASS_LOADER = "class-for-name-respects-class-loader";
private static final String EXACT_REFLECTION = "exact-reflection";
public static final String EXPLICIT_FEATURE_SINGLETON_REGISTRATION = "explicit-feature-singleton-registration";
+ public static final String PREDICTABLE_BUILD_TIME_INITIALIZATION = "predictable-build-time-initialization";
private static final List ALL_FUTURE_DEFAULTS = List.of(CLASS_FOR_NAME_RESPECTS_CLASS_LOADER, EXACT_REFLECTION, RUN_TIME_INITIALIZE_FILE_SYSTEM_PROVIDERS,
- RUN_TIME_INITIALIZE_SECURITY_PROVIDERS, RUN_TIME_INITIALIZE_RESOURCE_BUNDLES, EXPLICIT_FEATURE_SINGLETON_REGISTRATION);
+ RUN_TIME_INITIALIZE_SECURITY_PROVIDERS, RUN_TIME_INITIALIZE_RESOURCE_BUNDLES, EXPLICIT_FEATURE_SINGLETON_REGISTRATION, PREDICTABLE_BUILD_TIME_INITIALIZATION);
private static final String COMPLETE_REFLECTION_TYPES = "complete-reflection-types";
private static final List RETIRED_FUTURE_DEFAULTS = List.of(COMPLETE_REFLECTION_TYPES);
@@ -281,6 +282,10 @@ public static boolean explicitFeatureSingletonRegistration() {
return getFutureDefaults().contains(EXPLICIT_FEATURE_SINGLETON_REGISTRATION);
}
+ public static boolean predictableBuildTimeInitialization() {
+ return getFutureDefaults().contains(PREDICTABLE_BUILD_TIME_INITIALIZATION);
+ }
+
/** Keep in sync with the static query methods from {@link #allFutureDefaults()} to the end of this class. */
@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class)
@Platforms(Platform.HOSTED_ONLY.class)
@@ -323,5 +328,10 @@ public boolean exactReflection() {
public boolean explicitFeatureSingletonRegistration() {
return FutureDefaultsOptions.explicitFeatureSingletonRegistration();
}
+
+ @Override
+ public boolean predictableBuildTimeInitialization() {
+ return FutureDefaultsOptions.predictableBuildTimeInitialization();
+ }
}
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt
index d80815266a737..ceb75bda78834 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/doc-files/FutureDefaultsHelp.txt
@@ -1,4 +1,4 @@
-Enable options that are planned to become defaults in future releases. Comma-separated list can contain 'all', 'none', 'run-time-initialize-jdk', 'class-for-name-respects-class-loader', 'exact-reflection', 'run-time-initialize-file-system-providers', 'run-time-initialize-security-providers', 'run-time-initialize-resource-bundles', 'explicit-feature-singleton-registration'. The preferred usage is '--future-defaults=all'.
+Enable options that are planned to become defaults in future releases. Comma-separated list can contain 'all', 'none', 'run-time-initialize-jdk', 'class-for-name-respects-class-loader', 'exact-reflection', 'run-time-initialize-file-system-providers', 'run-time-initialize-security-providers', 'run-time-initialize-resource-bundles', 'explicit-feature-singleton-registration', 'predictable-build-time-initialization'. The preferred usage is '--future-defaults=all'.
The meaning of each possible option is as follows:
'all' - is the preferred option, and it enables all other behaviors.
@@ -18,3 +18,5 @@ The meaning of each possible option is as follows:
'run-time-initialize-resource-bundles' - shifts away from build-time initialization for 'java.util.ResourceBundle'. Unless you store 'ResourceBundle'-related classes in the image heap, this option should not affect you. In case this option breaks your build, follow the suggestions in the error messages.
'explicit-feature-singleton-registration' - stops automatically publishing user features reached from '--features' as 'ImageSingletons'. If code needs feature-singleton lookup compatibility, the feature must register its feature object explicitly.
+
+ 'predictable-build-time-initialization' - verifies that the classes of all objects stored in or reachable from static fields of build-time-initialized classes are also initialized at build time, even if those fields are not reachable.
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java
index 8693004b57564..610675939540d 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/feature/JVMCIFeatureAccess.java
@@ -142,11 +142,23 @@ public interface DuringSetupAccess extends FeatureAccess {
void registerBuildTimeBootstrapCondy(ResolvedJavaMethod method);
}
+ /**
+ * JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeFieldValueAccess}.
+ */
+ @Platforms(Platform.HOSTED_ONLY.class)
+ public interface BeforeFieldValueAccess {
+ /**
+ * JVMCI-based counterpart of
+ * {@link org.graalvm.nativeimage.hosted.Feature.BeforeFieldValueAccess#registerFieldValueTransformer(java.lang.reflect.Field, org.graalvm.nativeimage.hosted.FieldValueTransformer)}.
+ */
+ void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer);
+ }
+
/**
* JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess}.
*/
@Platforms(Platform.HOSTED_ONLY.class)
- public interface BeforeAnalysisAccess extends FeatureAccess {
+ public interface BeforeAnalysisAccess extends FeatureAccess, BeforeFieldValueAccess {
/**
* JVMCI-based counterpart of {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerAsUsed(Class)}.
*/
@@ -205,9 +217,10 @@ void registerClassInitializerReachabilityHandler(Consumer
/**
* JVMCI-based counterpart of
- * {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerFieldValueTransformer(java.lang.reflect.Field, org.graalvm.nativeimage.hosted.FieldValueTransformer)}.
+ * {@link org.graalvm.nativeimage.hosted.Feature.BeforeAnalysisAccess#registerBeforeFieldValueAccessHandler(BiConsumer, Class)}.
*/
- void registerFieldValueTransformer(ResolvedJavaField field, JVMCIFieldValueTransformer transformer);
+ void registerBeforeFieldValueAccessHandler(BiConsumer callback,
+ ResolvedJavaType baseType);
}
/**
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java
index 0d711ac5cef27..23e2420e1706c 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/FieldOffsetFieldValueTransformer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,8 @@
*/
package com.oracle.svm.core.fieldvaluetransformer;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
+
import com.oracle.svm.shared.BuildPhaseProvider;
import com.oracle.svm.core.StaticFieldsSupport;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
@@ -40,7 +42,7 @@
/**
* Implements the field value transformation semantics of {@link Kind#FieldOffset}.
*/
-public record FieldOffsetFieldValueTransformer(ResolvedJavaField targetField, JavaKind returnKind) implements JVMCIFieldValueTransformerWithAvailability {
+public record FieldOffsetFieldValueTransformer(ResolvedJavaField targetField, JavaKind returnKind) implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse {
@Override
public boolean isAvailable() {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java
index 35b919dbedabd..dbba06d4f99bc 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/fieldvaluetransformer/StaticFieldBaseFieldValueTransformer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -28,6 +28,8 @@
import com.oracle.svm.core.StaticFieldsSupport;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
+
import jdk.graal.compiler.nodes.ValueNode;
import jdk.graal.compiler.nodes.spi.CoreProviders;
import jdk.vm.ci.meta.JavaConstant;
@@ -36,7 +38,7 @@
/**
* Implements the field value transformation semantics of {@link Kind#StaticFieldBase}.
*/
-public record StaticFieldBaseFieldValueTransformer(ResolvedJavaField targetField) implements JVMCIFieldValueTransformerWithAvailability {
+public record StaticFieldBaseFieldValueTransformer(ResolvedJavaField targetField) implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse {
@Override
public boolean isAvailable() {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java
index f59e11bd1ae24..a60c0f1a42032 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/GCCause.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -51,6 +51,7 @@
import com.oracle.svm.shared.singletons.traits.SingletonLayeredInstallationKind.InitialLayerOnly;
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
import com.oracle.svm.shared.util.VMError;
+import com.oracle.svm.util.HeapObjectReplacer;
import jdk.graal.compiler.debug.Assertions;
@@ -159,7 +160,16 @@ public void duringSetup(DuringSetupAccess access) {
* In traditional builds we lazily register GCCauses as they become visible to the
* native-image generator.
*/
- access.registerObjectReplacer(ImageSingletons.lookup(GCCauseSupport.class)::collectGCCauses);
+ GCCauseSupport support = ImageSingletons.lookup(GCCauseSupport.class);
+ access.registerObjectReplacer(new HeapObjectReplacer() {
+ @Override
+ public Object replaceObject(Object source, boolean reachable) {
+ if (reachable) {
+ support.collectGCCauses(source);
+ }
+ return source;
+ }
+ });
} else {
/*
* For layered builds we eagerly register all GCCauses in the initial layer. In all
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java
index f6f2871bf5609..3c71dd78e574a 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/ServiceCatalogSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -34,6 +34,7 @@
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
import org.graalvm.nativeimage.hosted.Feature;
+import org.graalvm.nativeimage.hosted.FieldValueTransformer;
import com.oracle.svm.core.encoder.SymbolEncoder;
import com.oracle.svm.shared.singletons.AutomaticallyRegisteredImageSingleton;
@@ -67,9 +68,10 @@ public void removeServicesFromServicesCatalog(String serviceProvider, Set {
+ class ServiceCatalogMapFieldValueTransformer implements FieldValueTransformer {
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object transform(Object receiver, Object original) {
VMError.guarantee(sealed);
ConcurrentHashMap> map = (ConcurrentHashMap>) original;
final ConcurrentHashMap> res = new ConcurrentHashMap<>();
@@ -83,8 +85,19 @@ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess acce
}
});
return res;
- });
- access.registerFieldValueTransformer(ReflectionUtil.lookupField(ModuleDescriptor.Provides.class, "providers"), (receiver, original) -> {
+ }
+
+ @Override
+ public boolean isAvailable() {
+ /* Available after it is sealed, because it can be read before analysis by PBTI */
+ return sealed;
+ }
+ }
+
+ class ModuleDescriptorProvidersFieldValueTransformer implements FieldValueTransformer {
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object transform(Object receiver, Object original) {
VMError.guarantee(sealed, "Service provider detector must be registered before the analysis starts");
List providers = (List) original;
String service = ((ModuleDescriptor.Provides) receiver).service();
@@ -94,7 +107,17 @@ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess acce
}
SymbolEncoder encoder = SymbolEncoder.singleton();
return providers.stream().map(encoder::encodeClass).toList();
- });
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return sealed;
+ }
+ }
+
+ public void enableServiceCatalogMapTransformer(Feature.BeforeAnalysisAccess access) {
+ access.registerFieldValueTransformer(ReflectionUtil.lookupField(ServicesCatalog.class, "map"), new ServiceCatalogMapFieldValueTransformer());
+ access.registerFieldValueTransformer(ReflectionUtil.lookupField(ModuleDescriptor.Provides.class, "providers"), new ModuleDescriptorProvidersFieldValueTransformer());
registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "requires");
registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "exports");
registerModuleDescriptorSetTransformer(access, ModuleDescriptor.class, "opens");
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java
index 3b56431daf0c6..ba691c1a0c7b7 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/Target_jdk_internal_module_ModuleReaders.java
@@ -144,7 +144,7 @@ final class Target_jdk_internal_module_ModuleReferences_JarModuleReader {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.None, isFinal = false) JarFile jf;
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.None, isFinal = false) URI uri;
- @Inject String runtimeModuleName;
+ @Inject @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset, isFinal = false) String runtimeModuleName;
@Alias
@TargetElement(name = "newJarFile")
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java
index c203b8b38cd4d..e76e9c45d9c4d 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/VarHandleSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
package com.oracle.svm.core.jdk;
import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
import com.oracle.svm.shared.BuildPhaseProvider;
import com.oracle.svm.core.StaticFieldsSupport;
@@ -109,7 +110,7 @@ class VarHandleFieldOffsetAsLongComputer extends VarHandleFieldOffsetComputer {
}
}
-class VarHandleStaticBaseComputer implements JVMCIFieldValueTransformerWithAvailability {
+class VarHandleStaticBaseComputer implements JVMCIFieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse {
@Override
public boolean isAvailable() {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java
index 351319617b521..c9ee13b0094f6 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/locks/ClassInstanceReplacer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,41 +26,58 @@
import java.util.Collections;
import java.util.IdentityHashMap;
-import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Function;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
+import com.oracle.svm.util.HeapObjectReplacer;
+
@Platforms(Platform.HOSTED_ONLY.class)
-public final class ClassInstanceReplacer implements Function
+ */
+@AutomaticallyRegisteredFeature
+@SingletonTraits(access = BuiltinTraits.BuildtimeAccessOnly.class, layeredCallbacks = BuiltinTraits.NoLayeredCallbacks.class)
+@SuppressWarnings("unused")
+public class PredictableBuildTimeInitializationFeature implements InternalFeature {
+ private static final int FIELD_BATCH_SIZE = 32;
+
+ public static class Options {
+ // @formatter:off
+ /**
+ * Full reports for failures discovered before analysis are generated and reported immediately.
+ * Failures discovered after analysis are retained and included in the final report before image writing.
+ */
+ @Option(help = "Create a full report of predictable build-time initialization failures, including all reconstructed paths", stability = OptionStability.EXPERIMENTAL)
+ public static final HostedOptionKey PBTIFullReport = new HostedOptionKey<>(true);
+
+ /**
+ * It is better to limit this number as the number of explored branches can be quite substantial in some cases.
+ * For {@link PredictableBuildTimeInitializationFeature pbti} purposes,
+ * one path per one root is enough to make decisions about class initialization configuration.
+ */
+ @Option(help = "Maximum number of alternate path branches explored per offending type and root when reconstructing a full PBTI report. Use -1 for no limit.", stability = OptionStability.EXPERIMENTAL)
+ public static final HostedOptionKey PBTIMaximumPathBranches = new HostedOptionKey<>(100, optionKey -> {
+ int value = optionKey.getValue();
+ if (value < -1) {
+ throw UserError.invalidOptionValue(optionKey, value, "The value must be -1 or greater.");
+ }
+ });
+
+ /**
+ * This option stops class initialization being gated by reachability.
+ * It can also interfere with {@link com.oracle.svm.hosted.classinitialization.ClassInitializationOptions#AssertInitializationSpecifiedForAllClasses}.
+ */
+ @Option(help = "Initialize all possible roots for PBTI", stability = OptionStability.EXPERIMENTAL)
+ public static final HostedOptionKey InitializeNewRoots = new HostedOptionKey<>(true);
+ // @formatter:on
+ }
+
+ protected ClassInitializationSupport classInitializationSupport;
+ private AnalysisUniverse universe;
+ private MetaAccessProvider origMetaAccess;
+ protected SVMHostedValueProvider hostedValueProvider;
+ private FieldValueInterceptionSupport fieldValueInterceptionSupport;
+ private GuestTypes guestTypes;
+
+ /**
+ * Custom type layer for pbti representing types after substitution was performed,
+ * and to prevent type lookups from expanding the analysis universe.
+ */
+ private PredictableBuildTimeInitializationLookupLayer pbtiLookupLayer;
+
+ private CompletionExecutor executor;
+
+ /**
+ * Set of all types that were already processed.
+ */
+ protected final Set processedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>());
+
+ /**
+ * Set of processed objects read from fields to prevent cyclic dependency between objects.
+ */
+ protected final Set processedConstants = Collections.newSetFromMap(new ConcurrentHashMap<>());
+
+ /**
+ * Extra paths that have encountered the same constant. Used for path reconstruction.
+ */
+ private final Map> extraPathsToConstant = new ConcurrentHashMap<>();
+
+ /**
+ * Map of run-time initialized types found at paths accessed from roots.
+ */
+ protected final Map> buildTimeFailures = new ConcurrentHashMap<>();
+
+ /**
+ * Disallowed objects found while traversing the fields of build-time initialized objects.
+ */
+ private final Map> disallowedObjectFailures = new ConcurrentIdentityHashMap<>();
+
+ /**
+ * Field reads delayed by {@link FieldValueTransformerWithAvailability#isAvailable()}.
+ */
+ private List>> delayedFieldValues = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Layered field values that are not currently available.
+ * These are separate from normal delayed fields because unavailable layered values can be
+ * silently skipped in beforeImageWrite.
+ */
+ private List>> delayedLayeredFieldValues = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Fields that require the analysis to be processed any further.
+ * This is done based on {@link FieldValueTransformerRequiresUniverse},
+ * if a field is read and the receiver is reachable, then it can be checked further.
+ */
+ private List> analysisFieldValues = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Values whose constant replacer can only run after analysis has finished.
+ */
+ private List> analysisReplacementValues = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Values of renamed types deferred so PBTI cannot consume names before analysis does.
+ */
+ private List> analysisDeferredValues = Collections.synchronizedList(new ArrayList<>());
+
+ /**
+ * Roots discovered by type creation callbacks, which can run concurrently.
+ */
+ private final Queue createdTypes = new ConcurrentLinkedQueue<>();
+
+ protected static final String[] EXCLUDED_ROOTS = {
+ /*
+ * Field nativeInstrumentedFilterState contains ByteBuffer,
+ * which pbti can find, and is disallowed by DisallowedImageHeapObjects check.
+ */
+ "com.oracle.truffle.runtime.debug.JFRListener",
+ };
+
+ /**
+ * Array of type names that are ignored from further processing, when an instance is found.
+ * This can only be used for internal classes that might be misconfigured.
+ */
+ protected static final String[] IGNORED_TYPES = {
+ /*
+ * This class is not marked as build-time initialized, when Truffle's
+ * initialization configuration is missing. An instance can be found from:
+ * - "com.oracle.svm.truffle.nfi.ErrnoMirrorGen"
+ * - "com.oracle.svm.truffle.nfi.KeysArrayGen"
+ */
+ "com.oracle.truffle.api.library.DynamicDispatchLibraryGen",
+ /*
+ * When InitializeNewRoots is enabled, this class instance can be found from these roots
+ * - org.graalvm.polyglot.Engine
+ * - com.oracle.truffle.polyglot.PolyglotLanguageContext
+ * - com.oracle.truffle.polyglot.PolyglotContextImpl
+ * - com.oracle.truffle.api.TruffleLogger$LoggerCache
+ * - com.oracle.truffle.sl.runtime.SLFunction
+ * And these violating objects can be found active:
+ * - jdk.internal.jimage.ImageBufferCache$1
+ * - started Thread instance
+ * - ByteBuffer
+ * - instance of Cleanable or jdk.internal.ref.Cleaner
+ */
+ "com.oracle.truffle.polyglot.PolyglotContextImpl"
+ };
+
+ private final class ProcessFieldsTask implements CompletionExecutor.DebugContextRunnable {
+ private final LinkedPath path;
+ private final InterceptedFieldType type;
+ private final JavaConstant receiver;
+
+ ProcessFieldsTask(LinkedPath path, InterceptedFieldType type) {
+ this(path, type, null);
+ }
+
+ ProcessFieldsTask(LinkedPath path, InterceptedFieldType type, JavaConstant receiver) {
+ this.path = path;
+ this.type = type;
+ this.receiver = receiver;
+ }
+
+ @Override
+ public void run(DebugContext debug) {
+ var fields = receiver == null ? type.getStaticFields() : type.getInstanceFields(true);
+ for (var field : fields) {
+ processField(new LinkedPath(path, new Field(type, (InterceptedField) field)), receiver);
+ }
+ }
+
+ @Override
+ public DebugContext.Description getDescription() {
+ if (receiver == null) {
+ return new DebugContext.Description(path, "PBTI static fields");
+ }
+
+ return new DebugContext.Description(path, "PBTI instance fields");
+ }
+ }
+
+ private final class ProcessDelayedFieldsTask implements CompletionExecutor.DebugContextRunnable {
+ private final List>> taskDelayedFields;
+
+ ProcessDelayedFieldsTask(List>> taskDelayedFields) {
+ this.taskDelayedFields = List.copyOf(taskDelayedFields);
+ }
+
+ @Override
+ public void run(DebugContext debug) {
+ for (Pair> delayedField : taskDelayedFields) {
+ if (!delayedField.getRight().isAvailable()) {
+ delayedFieldValues.add(delayedField);
+ continue;
+ }
+
+ processConstant(delayedField.getLeft(), getValueFromSupplier(delayedField.getLeft(), delayedField.getRight()));
+ }
+ }
+
+ @Override
+ public DebugContext.Description getDescription() {
+ return new DebugContext.Description(taskDelayedFields, "PBTI delayed fields");
+ }
+ }
+
+ private final class ProcessDelayedLayeredFieldsTask implements CompletionExecutor.DebugContextRunnable {
+ private final List>> taskDelayedFields;
+
+ ProcessDelayedLayeredFieldsTask(List>> taskDelayedFields) {
+ this.taskDelayedFields = List.copyOf(taskDelayedFields);
+ }
+
+ @Override
+ public void run(DebugContext debug) {
+ for (Pair> delayedField : taskDelayedFields) {
+ if (delayedField.getRight().isAvailable()) {
+ processConstant(delayedField.getLeft(), getValueFromSupplier(delayedField.getLeft(), delayedField.getRight()));
+ }
+ }
+ }
+
+ @Override
+ public DebugContext.Description getDescription() {
+ return new DebugContext.Description(taskDelayedFields, "PBTI delayed layered fields");
+ }
+ }
+
+ private final class ProcessAnalysisFieldsTask implements CompletionExecutor.DebugContextRunnable {
+ private final List> taskAnalysisFields;
+
+ ProcessAnalysisFieldsTask(List> taskAnalysisFields) {
+ this.taskAnalysisFields = List.copyOf(taskAnalysisFields);
+ }
+
+ @Override
+ public void run(DebugContext debug) {
+ for (Pair analysisField : taskAnalysisFields) {
+ processField(analysisField.getLeft(), analysisField.getRight());
+ }
+ }
+
+ @Override
+ public DebugContext.Description getDescription() {
+ return new DebugContext.Description(taskAnalysisFields, "PBTI analysis fields");
+ }
+ }
+
+ private final class ProcessAnalysisConstantsTask implements CompletionExecutor.DebugContextRunnable {
+ private final List> taskAnalysisReplacementValues;
+
+ ProcessAnalysisConstantsTask(List> taskAnalysisReplacementValues) {
+ this.taskAnalysisReplacementValues = List.copyOf(taskAnalysisReplacementValues);
+ }
+
+ @Override
+ public void run(DebugContext debug) {
+ for (Pair analysisReplacementValue : taskAnalysisReplacementValues) {
+ processConstant(analysisReplacementValue.getLeft(), analysisReplacementValue.getRight());
+ }
+ }
+
+ @Override
+ public DebugContext.Description getDescription() {
+ return new DebugContext.Description(taskAnalysisReplacementValues, "PBTI analysis constants");
+ }
+ }
+
+ /**
+ * Result of a constant read from {@link #resolveObjectConstant(LinkedPath, JavaConstant)}.
+ *
+ * @param path Where was the constant read from
+ * @param type Resolved type of the constant
+ * @param constant Constant object
+ */
+ private record ConstantInfo(LinkedPath path, InterceptedFieldType type, JavaConstant constant) {
+ }
+
+ /** Data for one member of a path, independent of the linked path structure. */
+ protected abstract static class PathMember implements Comparable {
+ private JavaConstant constant;
+ private InterceptedFieldType constantType;
+
+ /**
+ * @return Get the constant that was read at this path member.
+ */
+ public JavaConstant getConstant() {
+ assert constant != null;
+ return constant;
+ }
+
+ /**
+ * @param constant Set the constant that was read from this path member, after replacement
+ */
+ public void setConstant(JavaConstant constant, InterceptedFieldType constantType) {
+ this.constant = constant;
+ this.constantType = constantType;
+ }
+
+ public InterceptedFieldType getConstantType() {
+ assert constantType != null;
+ return constantType;
+ }
+
+ /**
+ * @return Whether there should be a dot between this and the previous member when formatting
+ * a path.
+ */
+ public boolean requiresTrailingDot() {
+ return true;
+ }
+
+ @Override
+ public abstract boolean equals(Object obj);
+
+ @Override
+ public abstract int hashCode();
+
+ @Override
+ public abstract String toString();
+
+ public String toString(boolean qualified) {
+ return toString();
+ }
+
+ @Override
+ public final int compareTo(PathMember other) {
+ if (this instanceof Index leftIndex && other instanceof Index rightIndex) {
+ return Integer.compare(leftIndex.value, rightIndex.value);
+ }
+
+ int result = Integer.compare(sortKind(), other.sortKind());
+ if (result != 0) {
+ return result;
+ }
+ return toString().compareTo(other.toString());
+ }
+
+ private int sortKind() {
+ return switch (this) {
+ case Root root -> 0;
+ case Field field -> 1;
+ case Index index -> 2;
+ default -> throw VMError.shouldNotReachHere("Unknown path member: " + this);
+ };
+ }
+ }
+
+ /**
+ * Path that was taken from {@link Root} to access a {@link Field} to check the build-time
+ * initialization status of the constant it holds.
+ */
+ protected static final class LinkedPath {
+ public final LinkedPath prev;
+ public final PathMember member;
+
+ /**
+ * The root at which this path starts at.
+ */
+ private final Root root;
+
+ LinkedPath(LinkedPath prev, PathMember member) {
+ this.prev = prev;
+ this.member = member;
+ this.root = prev == null ? (Root) member : prev.root;
+ }
+
+ @Override
+ public String toString() {
+ return toString(true);
+ }
+
+ public String toString(boolean qualified) {
+ return formatPath(getPathMembers(), qualified);
+ }
+
+ public List getPathMembers() {
+ var result = new ArrayList();
+ for (LinkedPath current = this; current != null; current = current.prev) {
+ result.add(current.member);
+ }
+ return result;
+ }
+
+ public InterceptedFieldType getRootType() {
+ return root.type;
+ }
+
+ /**
+ * @return The type of the constant that holds the field (or array) that
+ * the final constant, where an exception occurred, was read from.
+ */
+ public InterceptedFieldType getHolderType() {
+ for (LinkedPath path = this; path != null; path = path.prev) {
+ if (path.member instanceof Field field) {
+ return field.type;
+ }
+ }
+
+ throw VMError.shouldNotReachHere("No holder type found for path: " + this);
+ }
+
+ private static String formatPath(List path) {
+ return formatPath(path, true);
+ }
+
+ private static String formatPath(List path, boolean qualified) {
+ var result = new StringBuilder();
+ var rootIdx = path.size() - 1;
+ for (int i = rootIdx; i >= 0; i--) {
+ PathMember member = path.get(i);
+ if (i < rootIdx && member.requiresTrailingDot()) {
+ result.append('.');
+ }
+ result.append(member.toString(qualified));
+ }
+ return result.toString();
+ }
+ }
+
+ public static final class Root extends PathMember {
+ public InterceptedFieldType type;
+
+ Root(InterceptedFieldType type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return toString(true);
+ }
+
+ @Override
+ public String toString(boolean qualified) {
+ return type.toJavaName(qualified);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Root root && root.type.equals(type);
+ }
+
+ @Override
+ public int hashCode() {
+ return type.hashCode();
+ }
+ }
+
+ public static final class Field extends PathMember {
+ public InterceptedFieldType type;
+ public InterceptedField field;
+
+ Field(InterceptedFieldType type, InterceptedField field) {
+ this.type = type;
+ this.field = field;
+ }
+
+ @Override
+ public String toString() {
+ return field.getName();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Field f && f.field.equals(field) && f.type.equals(type);
+ }
+
+ @Override
+ public int hashCode() {
+ return field.hashCode();
+ }
+ }
+
+ public static final class Index extends PathMember {
+ public int value;
+
+ Index(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + value + "]";
+ }
+
+ @Override
+ public boolean requiresTrailingDot() {
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Index idx && idx.value == value;
+ }
+
+ @Override
+ public int hashCode() {
+ return value;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public final class DisallowedObjectException extends RuntimeException {
+ private final Object object;
+ private final InterceptedFieldType type;
+ private final String originalMessage;
+ private final String initializerAction;
+ private final MaterializedPath path;
+
+ private DisallowedObjectException(String message, Object object, String initializerAction, InterceptedFieldType type, MaterializedPath path) {
+ super(createMessage(message, object, initializerAction, type, path));
+ this.object = object;
+ this.initializerAction = initializerAction;
+ this.originalMessage = message;
+ this.type = type;
+ this.path = path;
+ }
+
+ public String getOriginalMessage() {
+ return originalMessage;
+ }
+
+ public Object getObject() {
+ return object;
+ }
+
+ public InterceptedFieldType getType() {
+ return type;
+ }
+
+ public String getPathString() {
+ return path.getPathString();
+ }
+
+ public MaterializedPath getPath() {
+ return path;
+ }
+
+ public String getInitializerAction() {
+ return initializerAction;
+ }
+
+ private static String createMessage(String message, Object object, String initializerAction, InterceptedFieldType type, MaterializedPath path) {
+ return type.toJavaName() + " reachable from " + path.getRootType().toJavaName() + " via " + path.getUnqualifiedPathString() + ": " + message + " " + initializerAction;
+ }
+ }
+
+ /**
+ * Exception thrown in an object replacer or a field value transformer.
+ * The exception trace is shown along with the traversal path and build-time initialized root.
+ * When the cause is a UserError instance, then a possible resolution is shown by initializing
+ * the root at run-time.
+ */
+ @SuppressWarnings("serial")
+ public static final class TraversalException extends UnsupportedFeatureException {
+ private final MaterializedPath path;
+
+ /**
+ * Different from {@code cause} to shorten the error message,
+ * because it is already shown there.
+ */
+ private final Throwable source;
+
+ private TraversalException(MaterializedPath path, Throwable cause) {
+ super(createMessage(path, cause));
+ this.path = path;
+ this.source = cause;
+ }
+
+ public String getPathString() {
+ return path.getPathString();
+ }
+
+ public Throwable getSourceException() {
+ return source;
+ }
+
+ private static String createMessage(MaterializedPath path, Throwable cause) {
+ StringBuilder message = new StringBuilder(
+ "An exception occurred while traversing an object accessible from a static field of a build-time initialized class.")
+ .append(System.lineSeparator()).append(System.lineSeparator())
+ .append("Found from the build-time initialized class '")
+ .append(path.getRootType().toJavaName())
+ .append("' through the path:")
+ .append(System.lineSeparator()).append(System.lineSeparator())
+ .append(" '").append(path.getUnqualifiedPathString()).append('\'')
+ .append(System.lineSeparator()).append(System.lineSeparator());
+
+ if (cause instanceof UserError.UserException) {
+ message
+ .append("Possible resolution:")
+ .append(System.lineSeparator())
+ .append(" The object was found through the build-time initialized class '")
+ .append(path.getRootType().toJavaName())
+ .append("'.")
+ .append(System.lineSeparator())
+ .append(" If this class does not need to be initialized at build time, the safer approach is to initialize it at run time with:")
+ .append(System.lineSeparator()).append(System.lineSeparator()).append(" ")
+ .append(SubstrateOptionsParser.commandArgument(ClassInitializationOptions.ClassInitialization,
+ path.getRootType().toJavaName(), "initialize-at-run-time", true, false))
+ .append(System.lineSeparator()).append(System.lineSeparator())
+ .append(" This prevents the root's static state from being persisted in the image heap.")
+ .append(System.lineSeparator()).append(System.lineSeparator());
+ }
+
+ return message.append("The traversal failed with:")
+ .append(System.lineSeparator())
+ .append(cause)
+ .toString();
+ }
+ }
+
+ @Override
+ public boolean isInConfiguration(IsInConfigurationAccess access) {
+ // return FutureDefaultsOptions.predictableBuildTimeInitialization();
+ return true;
+ }
+
+ @Override
+ public void onRegistration(OnRegistrationAccess access) {
+ ImageSingletons.add(PredictableBuildTimeInitializationFeature.class, this);
+ }
+
+ @Override
+ public void duringSetup(DuringSetupAccess a) {
+ var access = (FeatureImpl.DuringSetupAccessImpl) a;
+
+ classInitializationSupport = ClassInitializationSupport.singleton();
+ universe = access.getUniverse();
+ hostedValueProvider = (SVMHostedValueProvider) universe.getHostedValuesProvider();
+ fieldValueInterceptionSupport = FieldValueInterceptionSupport.singleton();
+ origMetaAccess = universe.getOriginalMetaAccess();
+ guestTypes = access.getImageClassLoader().guestTypes;
+
+ pbtiLookupLayer = new PredictableBuildTimeInitializationLookupLayer(
+ access.getHostVM(), origMetaAccess, universe.getSubstitutions(), universe.getWordKind());
+
+ executor = new CompletionExecutor(access.getDebugContext(), access.getBigBang());
+
+ if (Options.InitializeNewRoots.getValue()) {
+ access.registerOnTypeCreatedCallback((type) -> {
+ if (type.getWrapped() instanceof BaseLayerType) {
+ /*
+ * It is assumed that the type from previous layer
+ * was checked by the feature the layer was built.
+ * There is no extra re-processing for these types.
+ */
+ return;
+ }
+
+ var interceptedType = lookupType(OriginalClassProvider.getJavaClass(type));
+ if (processedTypes.contains(interceptedType)) {
+ return;
+ }
+
+ /* Run the potential initialization later */
+ createdTypes.add(type);
+ });
+ }
+ }
+
+ /**
+ * This method has to run after every beforeAnalysis feature method has run.
+ * Called in {@link com.oracle.svm.hosted.NativeImageGenerator}, after class
+ * initialization registration is sealed.
+ *
+ *
+ * The current processing goes as following:
+ *
+ *
Initialize new roots from {@link GuestTypes} in series, if -H:+InitializeNewRoots
+ *
Process fields in parallel before analysis
+ *
Process newly found roots by analysis after it completes
+ *
+ * Process delayed fields via {@link FieldValueTransformerWithAvailability}
+ * after compilation, before the post-compilation image heap verification.
+ *
+ *
Process delayed fields before heap layout, before the final image heap verification.
+ *
Process any remaining delayed fields before image write, where they must be available.
+ *
+ *
+ */
+ public void afterBeforeAnalysis() {
+ executor.init();
+
+ assert createdTypes.isEmpty();
+ processBuildTimeRoots();
+
+ if (Options.InitializeNewRoots.getValue()) {
+ /*
+ * Concurrent initialization can deadlock when class initializers contain cyclic
+ * dependencies, which happens for Truffle's Assumption classes.
+ */
+ for (ResolvedJavaType type : guestTypes.getApplicationTypes()) {
+ processUninitializedRoot(OriginalClassProvider.getJavaClass(type));
+ }
+ }
+
+ runExecutor();
+ throwIfFailure();
+ }
+
+ @Override
+ public void afterAnalysis(AfterAnalysisAccess access) {
+ executor.init();
+
+ processAnalysisFieldValues();
+ processAnalysisReplacementValues();
+ processDeferredValues();
+
+ processDelayedFieldValues();
+ processBuildTimeRoots();
+
+ if (Options.InitializeNewRoots.getValue()) {
+ AnalysisType type;
+ while ((type = createdTypes.poll()) != null) {
+ processUninitializedRoot(OriginalClassProvider.getJavaClass(type));
+ }
+ }
+
+ runExecutor();
+ if (!Options.PBTIFullReport.getValue()) {
+ throwIfFailure();
+ }
+ }
+
+ /**
+ * Retry values that become available once compilation is finished. This must run before the
+ * post-compilation image heap verification so PBTI failures retain their rooted paths and are
+ * reported before the general image heap check.
+ */
+ public void afterCompilationFinishes() {
+ executor.init();
+
+ processDelayedFieldValues();
+ processDelayedLayeredFieldValues();
+ runExecutor();
+
+ throwIfFailure();
+ }
+
+ /**
+ * Run before the final heap verification is done and the heap is sealed.
+ */
+ @Override
+ public void beforeHeapLayout(BeforeHeapLayoutAccess access) {
+ executor.init();
+
+ processDelayedFieldValues();
+ processDelayedLayeredFieldValues();
+ runExecutor();
+
+ throwIfFailure();
+ }
+
+ @Override
+ public void beforeImageWrite(BeforeImageWriteAccess access) {
+ /*
+ * Values that can appear in the image heap must already be available because the image is about to be written.
+ * Process delayed fields in beforeImageWrite so that failures are reported before the executable is written out.
+ */
+ executor.init();
+
+ assert createdTypes.isEmpty();
+ processDelayedFieldValues();
+ processDelayedLayeredFieldValues();
+ runExecutor();
+
+ /* All fields must be processed by now */
+ VMError.guarantee(delayedFieldValues.isEmpty(),
+ "Field values are still unavailable before image writing: %n%s",
+ String.join("\n", delayedFieldValues.stream().map(Pair::getLeft).map(p -> " - " + p.toString()).toList()));
+
+ throwIfFailure();
+ }
+
+ /**
+ * Process the current set of build-time initialized classes.
+ */
+ protected void processBuildTimeRoots() {
+ for (Class> clazz : classInitializationSupport.classesWithKind(InitKind.BUILD_TIME)) {
+ InterceptedFieldType type = lookupType(clazz);
+ if (type == null) {
+ continue;
+ }
+
+ if (classInitializationSupport.computedInitKindFor(OriginalClassProvider.getJavaClass(type)) != InitKind.BUILD_TIME) {
+ /* The Target_ class is initialized, but the underlying class might not be. */
+ if (Options.InitializeNewRoots.getValue()) {
+ processUninitializedRoot(type);
+ }
+ continue;
+ }
+
+ processInitializedRoot(type);
+ }
+ }
+
+ /**
+ * Process a root synchronously and add its static fields to the executor.
+ */
+ protected final void processUninitializedRoot(Class> clazz) {
+ InterceptedFieldType type = lookupType(clazz);
+ if (type == null) {
+ return;
+ }
+
+ processUninitializedRoot(type);
+ }
+
+ /**
+ * Add root's static fields to the executor, ready for processing.
+ * A root is a type that is initialized at build time. If an attempt
+ * to initialize the type fails, then it is ignored.
+ */
+ private void processUninitializedRoot(InterceptedFieldType type) {
+ if (isExcludedRoot(type)) {
+ return;
+ }
+
+ if (isFromSharedLayer(type)) {
+ /*
+ * Shared layer types were initialized and verified while building the previous layer.
+ * Do not initialize or inspect their current hosted versions again. Generated types,
+ * such as Weld proxies, can be recreated under the same name with different metadata,
+ * which can make the build fail when compared with the persisted shared layer type.
+ */
+ return;
+ }
+
+ if (processedTypes.contains(type)) {
+ return;
+ }
+
+ if (ClassInitializationOptions.AssertInitializationSpecifiedForAllClasses.getValue() && !classInitializationSupport.canInitializeAtBuildTime(type)) {
+ /* Memoize the initialization result only if the class has been specified as BUILD_TIME */
+ return;
+ }
+
+ try {
+ var clazz = OriginalClassProvider.getJavaClass(type);
+ if (!classInitializationSupport.maybeInitializeAtBuildTime(clazz)) {
+ return;
+ }
+ } catch (UserError.UserException e) {
+ if (!e.toString().contains("Class initialization of ")) {
+ /* Re-throw other exceptions that are unrelated to class initialization */
+ throw e;
+ }
+
+ return;
+ }
+
+ processInitializedRoot(type);
+ }
+
+ private void processInitializedRoot(InterceptedFieldType type) {
+ if (isExcludedRoot(type)) {
+ return;
+ }
+
+ if (isFromSharedLayer(type)) {
+ return;
+ }
+
+ if (!processedTypes.add(type)) {
+ /* Using the substituted type for deduplication of processing. */
+ return;
+ }
+
+ var clazz = OriginalClassProvider.getJavaClass(type);
+ assert classInitializationSupport.computedInitKindFor(clazz) == InitKind.BUILD_TIME;
+
+ var root = new LinkedPath(null, new Root(type));
+ if (type.getStaticFields().length != 0) {
+ executor.execute(new ProcessFieldsTask(root, type));
+ }
+ }
+
+ private void runExecutor() {
+ executor.start();
+ try {
+ executor.complete();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ executor.shutdown();
+ }
+
+ private void processField(LinkedPath path, JavaConstant receiver) {
+ Field fieldMember = (Field) path.member;
+ InterceptedField field = fieldMember.field;
+ if (!field.getStorageKind().isObject()) {
+ return;
+ }
+
+ if (fieldValueInterceptionSupport.requiresAnalysis(field)) {
+ /*
+ * Cannot read this field without accessing the analysis.
+ * The field has to be read and the receiver must be reachable
+ * in order to proceed further.
+ *
+ * It is assumed that the SVMHostedValueProvider
+ * uses FieldValueInterceptionSupport to read field values.
+ */
+ if (!BuildPhaseProvider.isAnalysisFinished()) {
+ analysisFieldValues.add(Pair.create(path, receiver));
+ return;
+ }
+
+ if (!canProcessAnalysisField(field, receiver)) {
+ return;
+ }
+
+ field = getAnalysisField(field);
+ /*
+ * At this point, the field is known to be read and its receiver is reachable,
+ * therefore the value can be read using its field value transformer
+ * without expanding the image heap or the analysis/hosted universe through internal operations.
+ */
+ }
+
+ /* This must use the analysis field when a layered transformer needs to be installed. */
+ if (fieldValueInterceptionSupport.isValueNeverAvailable(field)) {
+ /* Explicitly ignore fields, that can never be read */
+ return;
+ }
+
+ /*
+ * Field value transformers are expected to be registered either
+ * in beforeAnalysis or in a beforeFieldValueAccess callback.
+ * Otherwise, no further registration is expected.
+ */
+ JavaConstant receiverToRead = receiver;
+ if (receiver instanceof ImageHeapConstant imageHeapConstant) {
+ assert imageHeapConstant.isBackedByHostedObject();
+ /* Cannot read the value from the VM if using heap constant */
+ receiverToRead = imageHeapConstant.getHostedObject();
+ }
+
+ ValueSupplier valueSupplier = hostedValueProvider.readFieldValue(field, receiverToRead);
+ if (!valueSupplier.isAvailable()) {
+ if (fieldValueInterceptionSupport.isLayeredFieldValue(field)) {
+ /* Retried before image write */
+ delayedLayeredFieldValues.add(Pair.create(path, valueSupplier));
+ } else {
+ delayedFieldValues.add(Pair.create(path, valueSupplier));
+ }
+ return;
+ }
+
+ JavaConstant constant = getValueFromSupplier(path, valueSupplier);
+ processConstant(path, constant);
+ }
+
+ private JavaConstant getValueFromSupplier(LinkedPath path, ValueSupplier valueSupplier) {
+ try {
+ return valueSupplier.get();
+ } catch (Throwable cause) {
+ throw new TraversalException(new MaterializedPath(path), cause);
+ }
+ }
+
+ /**
+ * All fields that were not available at the time of the read are re-processed. Fields that
+ * are still unavailable, as well as fields delayed while processing the newly available
+ * values, are retained for the next attempt.
+ */
+ private void processDelayedFieldValues() {
+ var previouslyDelayedFieldValues = delayedFieldValues;
+ delayedFieldValues = Collections.synchronizedList(new ArrayList<>());
+
+ processCollectionBatched(previouslyDelayedFieldValues, ProcessDelayedFieldsTask::new);
+ }
+
+ /**
+ * Retry layered field values once, ignoring values for which this layer has no update. Install
+ * a new list first so layered fields discovered transitively during processing can be skipped
+ * safely as well.
+ */
+ private void processDelayedLayeredFieldValues() {
+ var previouslyDelayedLayeredFieldValues = delayedLayeredFieldValues;
+ delayedLayeredFieldValues = Collections.synchronizedList(new ArrayList<>());
+
+ processCollectionBatched(previouslyDelayedLayeredFieldValues, ProcessDelayedLayeredFieldsTask::new);
+ }
+
+ /**
+ * Process fields whose transformers need analysis elements, after analysis has finished.
+ */
+ private void processAnalysisFieldValues() {
+ processCollectionBatched(analysisFieldValues, ProcessAnalysisFieldsTask::new);
+ analysisFieldValues = null; /* No longer needed */
+ }
+
+ /**
+ * Retry values whose constant replacement requires analysis reachability.
+ */
+ private void processAnalysisReplacementValues() {
+ processCollectionBatched(analysisReplacementValues, ProcessAnalysisConstantsTask::new);
+ analysisReplacementValues = null; /* No longer needed */
+ }
+
+ /**
+ * Retry values of renamed types, after analysis has finished.
+ */
+ private void processDeferredValues() {
+ processCollectionBatched(analysisDeferredValues, ProcessAnalysisConstantsTask::new);
+ analysisDeferredValues = null; /* No longer needed */
+ }
+
+ private void processCollectionBatched(List collection, Function, CompletionExecutor.DebugContextRunnable> taskCreator) {
+ for (int start = 0; start < collection.size(); start += FIELD_BATCH_SIZE) {
+ int end = Math.min(start + FIELD_BATCH_SIZE, collection.size());
+ executor.execute(taskCreator.apply(collection.subList(start, end)));
+ }
+ }
+
+ /**
+ * Process a constant by either turning it into a constant
+ * and calling {@link #processObjectConstant(LinkedPath, JavaConstant, InterceptedFieldType)}, or if
+ * said constant represents an array, then iterate until all
+ * objects are read from the array and processed.
+ *
+ * @param rootPath Path where the constant was read
+ * @param root Constant to be processed
+ */
+ private void processConstant(LinkedPath rootPath, JavaConstant root) {
+ /* Process the constant first, before allocating a deque */
+ ConstantInfo rootInfo = resolveObjectConstant(rootPath, root);
+ if (rootInfo == null) {
+ return;
+ }
+
+ if (!rootInfo.type().isArray()) {
+ processObjectConstant(rootInfo.path(), rootInfo.constant(), rootInfo.type());
+ return;
+ }
+
+ if (rootInfo.type().getComponentType().isPrimitive()) {
+ return;
+ }
+
+ var constWorklist = new ArrayDeque>();
+ addArrayElementsToWorklist(constWorklist, rootInfo.path(), rootInfo.constant());
+
+ while (!constWorklist.isEmpty()) {
+ var cp = constWorklist.poll();
+ ConstantInfo info = resolveObjectConstant(cp.getLeft(), cp.getRight());
+ if (info == null) {
+ continue;
+ }
+
+ if (!info.type().isArray()) {
+ processObjectConstant(info.path(), info.constant(), info.type());
+ continue;
+ }
+
+ if (info.type().getComponentType().isPrimitive()) {
+ continue;
+ }
+
+ addArrayElementsToWorklist(constWorklist, info.path(), info.constant());
+ }
+ }
+
+ private ConstantInfo resolveObjectConstant(LinkedPath path, JavaConstant constant) {
+ if (constant.isNull() || !constant.getJavaKind().isObject()) {
+ return null;
+ }
+
+ JavaConstant replacement;
+ if (constant instanceof ImageHeapConstant imageHeapConstant) {
+ replacement = imageHeapConstant;
+ } else {
+ replacement = replaceConstantNoHeap(path, constant);
+ if (HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER.equals(replacement)) {
+ if (!BuildPhaseProvider.isAnalysisFinished()) {
+ analysisReplacementValues.add(Pair.create(path, constant));
+ return null;
+ }
+
+ if (!canProcessAnalysisReplacement(path)) {
+ return null;
+ }
+
+ /*
+ * The constant cannot be replaced without internal operations that expand the analysis/hosted universe or image heap,
+ * so it is checked whether the nearest field that the constant was read from is read and its receiver is reachable.
+ * This increases the coverage of pbti without triggering those operations for unreachable objects.
+ */
+ replacement = replaceConstantWithAllReplacers(path, constant);
+ }
+ }
+
+ if (replacement.isNull()) {
+ return null;
+ }
+
+ if (replacement instanceof ImageHeapConstant imageHeapConstant && !imageHeapConstant.isBackedByHostedObject()) {
+ /* It is assumed that un-backed constants were already analyzed in the prior layer. */
+ return null;
+ }
+
+ if (!BuildPhaseProvider.isAnalysisFinished() && !(constant instanceof TypedConstant) && isDeferredType(origMetaAccess.lookupJavaType(constant))) {
+ /* Defer before substitution lookup and constant deduplication so the value is retried. */
+ analysisDeferredValues.add(Pair.create(path, replacement));
+ return null;
+ }
+
+ var type = lookupType(replacement);
+ if (type == null || !type.getStorageKind().isObject()) {
+ /*
+ * Platform unsupported and deleted types are ignored, these
+ * exceptions are thrown when their instances are visible to analysis.
+ */
+ return null;
+ }
+
+ /* For path reconstruction, the constant needs to be noted. */
+ path.member.setConstant(replacement, type);
+
+ JavaConstant constantIdentity = getConstantIdentity(replacement);
+ if (!processedConstants.add(constantIdentity)) {
+ if (Options.PBTIFullReport.getValue()) {
+ /* Skip extra processing, only take note of the path, if full report is done. */
+ extraPathsToConstant.computeIfAbsent(constantIdentity, _ -> new ConcurrentLinkedQueue<>()).add(path);
+ }
+ return null;
+ }
+
+ return new ConstantInfo(path, type, replacement);
+ }
+
+ private JavaConstant replaceConstantNoHeap(LinkedPath path, JavaConstant constant) {
+ try {
+ return universe.replaceConstantNoHeap(constant);
+ } catch (Throwable cause) {
+ throw new TraversalException(new MaterializedPath(path), cause);
+ }
+ }
+
+ private JavaConstant replaceConstantWithAllReplacers(LinkedPath path, JavaConstant constant) {
+ try {
+ return universe.replaceConstantWithAllReplacers(constant);
+ } catch (Throwable cause) {
+ throw new TraversalException(new MaterializedPath(path), cause);
+ }
+ }
+
+ private void addArrayElementsToWorklist(ArrayDeque> constWorklist, LinkedPath path, JavaConstant inputConstant) {
+ JavaConstant array = inputConstant;
+ if (array instanceof ImageHeapConstant imageHeapConstant) {
+ assert imageHeapConstant.isBackedByHostedObject();
+
+ array = imageHeapConstant.getHostedObject();
+ }
+
+ int length = hostedValueProvider.readArrayLength(array);
+ for (int i = 0; i < length; i++) {
+ JavaConstant element = hostedValueProvider.readArrayElement(array, i);
+ constWorklist.add(Pair.create(new LinkedPath(path, new Index(i)), element));
+ }
+ }
+
+ private int readArrayLength(JavaConstant inputConstant) {
+ JavaConstant array = inputConstant;
+ if (array instanceof ImageHeapConstant imageHeapConstant) {
+ if (imageHeapConstant instanceof ImageHeapArray imageHeapArray) {
+ return imageHeapArray.getLength();
+ }
+
+ array = imageHeapConstant.getHostedObject();
+ }
+ return hostedValueProvider.readArrayLength(array);
+ }
+
+ /**
+ * Check if constant's class is build-time initialized.
+ * If not, report a failure. If it is, read all its
+ * fields and schedule them for execution.
+ *
+ * @param path Where the constant was read from
+ * @param constant Constant to be processed
+ * @param type of the constant
+ */
+ private void processObjectConstant(LinkedPath path, JavaConstant constant, InterceptedFieldType type) {
+ assert !constant.isNull();
+
+ if (isIgnoredType(type)) {
+ return;
+ }
+
+ if (!maybeInitializeAtBuildTime(type)) {
+ buildTimeFailures
+ .computeIfAbsent(type, _ -> Collections.synchronizedList(new ArrayList<>()))
+ .add(path);
+ return;
+ }
+
+ var backingObject = getBackingObject(constant);
+ if (backingObject != null) {
+ DisallowedImageHeapObjects.DisallowedObjectReporter reporter = (msg, disallowedObject, initializerAction) -> {
+ return new DisallowedObjectException(msg, disallowedObject, initializerAction, type, new MaterializedPath(path));
+ };
+
+ try {
+ DisallowedImageHeapObjects.check(backingObject, reporter);
+ } catch (DisallowedObjectException exception) {
+ disallowedObjectFailures
+ .computeIfAbsent(exception.object, _ -> Collections.synchronizedList(new ArrayList<>()))
+ .add(exception);
+ return;
+ }
+ }
+
+ if (type.getInstanceFields(true).length != 0) {
+ executor.execute(new ProcessFieldsTask(path, type, constant));
+ }
+ }
+
+ private Object getBackingObject(JavaConstant constant) {
+ if (constant instanceof ImageHeapConstant imageHeapConstant) {
+ assert imageHeapConstant.isBackedByHostedObject();
+
+ return hostedValueProvider.asObject(Object.class, imageHeapConstant.getHostedObject());
+ }
+
+ return hostedValueProvider.asObject(Object.class, constant);
+ }
+
+ protected static JavaConstant getConstantIdentity(JavaConstant constant) {
+ JavaConstant uncompressed = CompressibleConstant.uncompress(constant);
+ if (uncompressed instanceof ImageHeapConstant imageHeapConstant) {
+ /* Assumption that un-backed hosted object has been skipped/processed in previous steps. */
+ assert imageHeapConstant.isBackedByHostedObject();
+
+ return imageHeapConstant.getHostedObject();
+ }
+ return uncompressed;
+ }
+
+ /**
+ * @return Whether this field that requires the universe can be further processed without
+ * internal operations expanding the analysis/hosted universe or image heap - the field is read
+ * and its receiver is reachable.
+ */
+ private boolean canProcessAnalysisField(InterceptedField field, JavaConstant receiver) {
+ var analysisField = getAnalysisField(field);
+ if (analysisField == null) {
+ return false;
+ }
+
+ if (!analysisField.isRead()) {
+ return false;
+ }
+
+ if (receiver == null) {
+ return true;
+ }
+
+ if (receiver instanceof ImageHeapConstant imageHeapConstant) {
+ return imageHeapConstant.isReachable();
+ }
+
+ Object receiverObject = hostedValueProvider.asObject(Object.class, receiver);
+ return universe.getHeapScanner().isObjectReachable(receiverObject);
+ }
+
+ private AnalysisField getAnalysisField(InterceptedField field) {
+ var pbtiField = (PredictableBuildTimeInitializationField) field;
+ return universe.getField(pbtiField.getWrapped());
+ }
+
+ /**
+ * @return Whether the nearest field is read and its receiver is reachable.
+ */
+ private boolean canProcessAnalysisReplacement(LinkedPath path) {
+ for (LinkedPath current = path; current != null; current = current.prev) {
+ /* Find the last field and check if it is read and its receiver is reachable. */
+ if (current.member instanceof Field fieldMember) {
+ JavaConstant receiver = null;
+ if (!fieldMember.field.isStatic()) {
+ assert current.prev != null : current;
+ receiver = current.prev.member.getConstant();
+ }
+ return canProcessAnalysisField(fieldMember.field, receiver);
+ }
+ }
+ throw VMError.shouldNotReachHere("No field found for path: " + path);
+ }
+
+ private static final int JSON_OUTPUT_VERSION = 1;
+
+ /**
+ * Write a versioned, machine-readable report. Run-time initialization failures are grouped by
+ * type and contain all paths to that type. Disallowed object failures have one entry per object.
+ *
+ *
+ *
+ * Where, {@code Root} is the build-time initialized root, {@code Object[2]} is the type of the array that
+ * was read from {@code field} field of {@code Root}.
+ *
+ *
+ * @param indentation Prefix for each line of the formatted path
+ * @return Formatted path
+ */
+ String getDetailedObjectPathString(String indentation) {
+ StringBuilder message = new StringBuilder();
+ for (int i = members.size() - 1; i >= 0; i--) {
+ PathMember member = members.get(i);
+ message.append(indentation).repeat(" ", members.size() - (i + 1));
+
+ switch (member) {
+ case Root root -> message.append(root.type.toJavaName());
+ case Field field -> message.append("- ").append(field.field.getName()).append(": ").append(getConstantTypeName(field));
+ case Index index -> message.append("- [").append(index.value).append("]: ").append(getConstantTypeName(index));
+ default -> {
+ }
+ }
+
+ message.append(System.lineSeparator());
+ }
+
+ return message.toString();
+ }
+
+ private String getConstantTypeName(PathMember member) {
+ InterceptedFieldType constantType = member.getConstantType();
+ String typeName = constantType.toJavaName();
+ if (!constantType.isArray()) {
+ return typeName;
+ }
+
+ int firstArrayDimension = typeName.indexOf("[]");
+ assert firstArrayDimension >= 0 : typeName;
+ return typeName.substring(0, firstArrayDimension) + '[' + readArrayLength(member.getConstant()) + ']' + typeName.substring(firstArrayDimension + 2);
+ }
+
+ @Override
+ public int compareTo(MaterializedPath other) {
+ int result = Integer.compare(members.size(), other.members.size());
+ if (result != 0) {
+ return result;
+ }
+
+ /* Members are in the reverse order */
+ for (int i = members.size() - 1; i >= 0; i--) {
+ result = members.get(i).compareTo(other.members.get(i));
+ if (result != 0) {
+ return result;
+ }
+ }
+ return 0;
+ }
+ }
+
+ private Map> materializeFailureMap(Map> failures) {
+ var result = new EconomicHashMap>();
+ failures.forEach((type, paths) -> result.put(type, paths.stream().map(MaterializedPath::new).toList()));
+ return result;
+ }
+
+ /**
+ * Sort failure map by offending type to make the errors (somewhat) stable across different builds.
+ */
+ private static Map> sortFailureMap(Map> failures) {
+ var result = new LinkedHashMap>(failures.size());
+ failures.entrySet().stream()
+ .sorted(Comparator.comparing(entry -> entry.getKey().toJavaName()))
+ .forEachOrdered(entry -> result.put(entry.getKey(), sortedPaths(entry.getValue())));
+ return Collections.unmodifiableMap(result);
+ }
+
+ /**
+ * Sort paths by their number of members and then by each member from root to leaf.
+ * To have access to the shortest path where a violation for certain type has occurred.
+ */
+ private static List sortedPaths(List paths) {
+ return paths.stream().sorted().toList();
+ }
+
+ private static Map> sortDisallowedObjectFailureMap(Map> failures) {
+ var result = new EconomicHashMap>(Equivalence.IDENTITY_WITH_SYSTEM_HASHCODE, failures.size());
+
+ // Compare by type name and sort individual paths
+ var comparator = Comparator.comparing((Pair> entry) -> entry.getRight().getFirst().type.toJavaName());
+ failures.entrySet().stream()
+ .map(entry -> Pair.create(entry.getKey(), sortedDisallowedObjectFailures(entry.getValue())))
+ .sorted(comparator)
+ .forEachOrdered(entry -> result.put(entry.getLeft(), entry.getRight()));
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ private static List sortedDisallowedObjectFailures(List failures) {
+ var comparator = Comparator.comparing((DisallowedObjectException failure) -> failure.type.toJavaName())
+ .thenComparing(failure -> failure.path);
+
+ return failures.stream().sorted(comparator).toList();
+ }
+
+ private record BranchCounterKey(InterceptedFieldType offendingType, InterceptedFieldType rootType) {
+ }
+
+ /** Entry for path reconstruction from {@link #extraPathsToConstant}. */
+ private class PathWorklistEntry {
+ private InterceptedFieldType holderType;
+ private final List members;
+ private final Set visitedConstants;
+ private final InterceptedFieldType offendingType;
+ private LinkedPath path;
+
+ PathWorklistEntry(LinkedPath path, InterceptedFieldType offendingType) {
+ this.members = new ArrayList<>();
+ this.visitedConstants = new EconomicHashSet<>();
+ this.path = path;
+ this.offendingType = offendingType;
+ }
+
+ PathWorklistEntry(PathWorklistEntry previous, LinkedPath path) {
+ this.holderType = previous.holderType;
+ this.members = new ArrayList<>(previous.members);
+ this.visitedConstants = new EconomicHashSet<>(previous.visitedConstants);
+ this.path = path;
+ this.offendingType = previous.offendingType;
+ }
+
+ private InterceptedFieldType getRoot() {
+ return path.getRootType();
+ }
+
+ private boolean hasNext() {
+ return path.prev != null;
+ }
+
+ private boolean next() {
+ if (!visitedConstants.add(getConstantIdentity(path.member.getConstant()))) {
+ return false;
+ }
+
+ if (holderType == null && path.member instanceof Field field) {
+ holderType = field.type;
+ }
+
+ members.add(path.member);
+ path = path.prev;
+ return true;
+ }
+
+ private MaterializedPath toResult() {
+ assert holderType != null;
+
+ members.add(path.member);
+ path = null;
+ return new MaterializedPath(holderType, members);
+ }
+ }
+
+ /**
+ * Reconstruct paths from the {@link LinkedPath} that have shared the same constant, via
+ * {@link #extraPathsToConstant}. Retain the shortest path from each root to an offending type,
+ * and sort the resulting paths so the shortest path is first.
+ */
+ private Map> reconstructAllFailurePaths() {
+ Map> pathsByRoot = new EconomicHashMap<>();
+ Map branchCounters = new EconomicHashMap<>();
+ Queue worklist = new ArrayDeque<>();
+ for (var failure : buildTimeFailures.entrySet()) {
+ for (var path : failure.getValue()) {
+ worklist.add(new PathWorklistEntry(path, failure.getKey()));
+ }
+ }
+
+ while (!worklist.isEmpty()) {
+ var entry = worklist.poll();
+
+ if (!entry.hasNext()) {
+ InterceptedFieldType root = entry.getRoot();
+ MaterializedPath reconstructedPath = entry.toResult();
+ pathsByRoot.computeIfAbsent(entry.offendingType, _ -> new EconomicHashMap<>())
+ .merge(root, reconstructedPath, (current, candidate) -> candidate.compareTo(current) < 0 ? candidate : current);
+ continue;
+ }
+
+ var extraPaths = extraPathsToConstant.get(getConstantIdentity(entry.path.member.getConstant()));
+ if (extraPaths != null) {
+ for (var branch : extraPaths) {
+ BranchCounterKey counterKey = new BranchCounterKey(entry.offendingType, branch.getRootType());
+ if (!canExerciseBranch(branchCounters, counterKey)) {
+ continue;
+ }
+ var branchEntry = new PathWorklistEntry(entry, branch);
+ if (branchEntry.next()) {
+ worklist.add(branchEntry);
+ }
+ }
+ }
+
+ if (entry.next()) {
+ worklist.add(entry);
+ }
+ }
+
+ Map> result = new EconomicHashMap<>();
+ pathsByRoot.forEach((type, rootPaths) -> result.put(type, new ArrayList<>(rootPaths.values())));
+ return sortFailureMap(result);
+ }
+
+ private static boolean canExerciseBranch(Map branchCounters, BranchCounterKey counterKey) {
+ if (Options.PBTIMaximumPathBranches.getValue() < 0) {
+ return true;
+ }
+
+ int branchCount = branchCounters.getOrDefault(counterKey, 0);
+ if (branchCount >= Options.PBTIMaximumPathBranches.getValue()) {
+ return false;
+ }
+
+ branchCounters.put(counterKey, branchCount + 1);
+ return true;
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java
new file mode 100644
index 0000000000000..2b3f08d56bbe4
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationField.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted.classinitialization.pbti;
+
+import java.lang.reflect.Modifier;
+
+import com.oracle.svm.util.InterceptedField;
+import com.oracle.graal.pointsto.infrastructure.WrappedJavaField;
+import com.oracle.svm.util.OriginalClassProvider;
+import com.oracle.svm.util.OriginalFieldProvider;
+
+import jdk.vm.ci.meta.JavaConstant;
+import jdk.vm.ci.meta.JavaKind;
+import jdk.vm.ci.meta.JavaType;
+import jdk.vm.ci.meta.ResolvedJavaField;
+import jdk.vm.ci.meta.ResolvedJavaType;
+import jdk.vm.ci.meta.annotation.Annotated;
+
+/**
+ * Field implementing {@link InterceptedField} to be able to read field without
+ * accessing analysis elements in {@link com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport}.
+ */
+public final class PredictableBuildTimeInitializationField implements ResolvedJavaField, WrappedJavaField, OriginalFieldProvider, InterceptedField {
+
+ private final PredictableBuildTimeInitializationLookupLayer lookupLayer;
+ private final ResolvedJavaField wrapped;
+ private final PredictableBuildTimeInitializationType declaringClass;
+ private final PredictableBuildTimeInitializationType fieldType;
+
+ private volatile Object fieldValueInterceptor;
+
+ PredictableBuildTimeInitializationField(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaField wrapped) {
+ this.lookupLayer = lookupLayer;
+ this.wrapped = wrapped;
+ this.declaringClass = lookupLayer.lookup(wrapped.getDeclaringClass());
+ this.fieldType = resolveFieldType(lookupLayer, wrapped);
+ }
+
+ PredictableBuildTimeInitializationLookupLayer getLookupLayer() {
+ return lookupLayer;
+ }
+
+ @Override
+ public ResolvedJavaField getWrapped() {
+ return wrapped;
+ }
+
+ @Override
+ public ResolvedJavaField unwrapTowardsOriginalField() {
+ return wrapped;
+ }
+
+ @Override
+ public Object getFieldValueInterceptor() {
+ return fieldValueInterceptor;
+ }
+
+ @Override
+ public void setFieldValueInterceptor(Object fieldValueInterceptor) {
+ this.fieldValueInterceptor = fieldValueInterceptor;
+ }
+
+ @Override
+ public String getName() {
+ return wrapped.getName();
+ }
+
+ @Override
+ public PredictableBuildTimeInitializationType getType() {
+ return fieldType;
+ }
+
+ @Override
+ public int getModifiers() {
+ return wrapped.getModifiers();
+ }
+
+ @Override
+ public int getOffset() {
+ return wrapped.getOffset();
+ }
+
+ @Override
+ public boolean isInternal() {
+ return wrapped.isInternal();
+ }
+
+ @Override
+ public boolean isSynthetic() {
+ return wrapped.isSynthetic();
+ }
+
+ @Override
+ public PredictableBuildTimeInitializationType getDeclaringClass() {
+ return declaringClass;
+ }
+
+ @Override
+ public JavaConstant getConstantValue() {
+ return wrapped.getConstantValue();
+ }
+
+ @Override
+ public JavaKind getStorageKind() {
+ return fieldType.getStorageKind();
+ }
+
+ @Override
+ public boolean isStatic() {
+ return Modifier.isStatic(getModifiers());
+ }
+
+ @Override
+ public void beforeFieldValueAccess() {
+ lookupLayer.hostVM().beforeFieldValueAccess(declaringClass);
+ }
+
+ @Override
+ public String toString() {
+ return "PBTIField<" + format("%h.%n") + " -> " + wrapped + ">";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this;
+ }
+
+ @Override
+ public int hashCode() {
+ return wrapped.hashCode();
+ }
+
+ @Override
+ public Annotated getWrappedAnnotated() {
+ return wrapped;
+ }
+
+ /**
+ * Resolve field type similarly to {@code com.oracle.graal.pointsto.meta.AnalysisField#getDeclaredType}.
+ */
+ private static PredictableBuildTimeInitializationType resolveFieldType(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaField field) {
+ try {
+ JavaType type = field.getType();
+ ResolvedJavaType resolvedType = type.resolve(OriginalClassProvider.getOriginalType(field.getDeclaringClass()));
+ PredictableBuildTimeInitializationType fieldType = lookupLayer.lookup(resolvedType);
+ return fieldType != null ? fieldType : lookupLayer.objectType();
+ } catch (LinkageError e) {
+ return lookupLayer.objectType();
+ }
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java
new file mode 100644
index 0000000000000..6feb05855952a
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationLookupLayer.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted.classinitialization.pbti;
+
+import java.lang.reflect.Field;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import com.oracle.svm.shared.util.VMError;
+import org.graalvm.word.WordBase;
+
+import com.oracle.graal.pointsto.api.HostVM;
+import com.oracle.graal.pointsto.constraints.UnsupportedPlatformException;
+import com.oracle.graal.pointsto.heap.TypedConstant;
+import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor;
+import com.oracle.svm.util.OriginalClassProvider;
+import com.oracle.svm.util.OriginalFieldProvider;
+
+import jdk.vm.ci.meta.JavaConstant;
+import jdk.vm.ci.meta.JavaKind;
+import jdk.vm.ci.meta.JavaType;
+import jdk.vm.ci.meta.MetaAccessProvider;
+import jdk.vm.ci.meta.ResolvedJavaField;
+import jdk.vm.ci.meta.ResolvedJavaType;
+
+/**
+ * Lookup layer used by {@link PredictableBuildTimeInitializationFeature} to access substituted
+ * fields and types without touching the analysis. The pbti feature can find new types, that
+ * the analysis has not yet seen, but accessing them via the {@link com.oracle.graal.pointsto.meta.AnalysisUniverse}
+ * can result in an exception being thrown, after the universe is sealed.
+ */
+public final class PredictableBuildTimeInitializationLookupLayer {
+
+ private final HostVM hostVM;
+ private final SubstitutionProcessor substitutions;
+ private final MetaAccessProvider wrappedMetaAccess;
+
+ private final ConcurrentMap types = new ConcurrentHashMap<>();
+ private final ConcurrentMap fields = new ConcurrentHashMap<>();
+
+ private final JavaKind wordKind;
+ private final ResolvedJavaType wordType;
+ private final PredictableBuildTimeInitializationType objectType;
+
+ public PredictableBuildTimeInitializationLookupLayer(HostVM hostVM, MetaAccessProvider wrappedMetaAccess, SubstitutionProcessor substitutions, JavaKind wordKind) {
+ this.hostVM = hostVM;
+ this.substitutions = substitutions;
+ this.wrappedMetaAccess = wrappedMetaAccess;
+
+ this.wordKind = wordKind;
+ this.wordType = wrappedMetaAccess.lookupJavaType(WordBase.class);
+ this.objectType = lookup(wrappedMetaAccess.lookupJavaType(Object.class));
+ }
+
+ public HostVM hostVM() {
+ return hostVM;
+ }
+
+ public SubstitutionProcessor getSubstitutions() {
+ return substitutions;
+ }
+
+ public MetaAccessProvider getOriginalMetaAccess() {
+ return wrappedMetaAccess;
+ }
+
+ public JavaKind getWordKind() {
+ return wordKind;
+ }
+
+ public PredictableBuildTimeInitializationType lookup(JavaConstant constant) {
+ if (constant == null || constant.getJavaKind() != JavaKind.Object || constant.isNull()) {
+ return null;
+ }
+ ResolvedJavaType type = constant instanceof TypedConstant typedConstant ? typedConstant.getType() : wrappedMetaAccess.lookupJavaType(constant);
+ return lookup(type);
+ }
+
+ public PredictableBuildTimeInitializationType lookup(JavaType type) {
+ JavaType result = lookupAllowUnresolved(type);
+ if (result == null) {
+ return null;
+ } else if (result instanceof PredictableBuildTimeInitializationType pbtiType) {
+ return pbtiType;
+ }
+ throw VMError.shouldNotReachHere("Unresolved type found: " + type.toJavaName(true));
+ }
+
+ public PredictableBuildTimeInitializationType lookup(ResolvedJavaType type) {
+ if (type == null) {
+ return null;
+ }
+ if (type instanceof PredictableBuildTimeInitializationType pbtiType) {
+ return pbtiType;
+ }
+
+ ResolvedJavaType originalType = OriginalClassProvider.getOriginalType(type);
+ PredictableBuildTimeInitializationType result = types.get(originalType);
+ if (result == null) {
+ ResolvedJavaType substitutedType = substitutions.lookup(originalType);
+ result = createType(originalType, substitutedType);
+ }
+ return result;
+ }
+
+ private PredictableBuildTimeInitializationType createType(ResolvedJavaType originalType, ResolvedJavaType substitutedType) {
+ if (!isPlatformSupported(originalType)) {
+ throw new UnsupportedPlatformException("Type is not available in this platform: " + originalType.toJavaName(true));
+ }
+
+ JavaKind storageKind;
+ if (wordType.isAssignableFrom(originalType)) {
+ storageKind = wordKind;
+ } else {
+ storageKind = substitutedType.getJavaKind();
+ }
+
+ var newType = new PredictableBuildTimeInitializationType(this, substitutedType, storageKind);
+ return types.computeIfAbsent(originalType, _ -> newType);
+ }
+
+ /**
+ * Check a type and its interface hierarchy for platform support. The superclass is eagerly
+ * looked up by {@link PredictableBuildTimeInitializationType} and checked separately.
+ */
+ private boolean isPlatformSupported(ResolvedJavaType type) {
+ if (!hostVM.platformSupported(type)) {
+ return false;
+ }
+
+ var interfaces = type.getInterfaces();
+ var typeWorklist = new ArrayDeque<>(Arrays.asList(interfaces));
+
+ while (!typeWorklist.isEmpty()) {
+ var iface = typeWorklist.poll();
+ if (!hostVM.platformSupported(iface)) {
+ return false;
+ }
+
+ typeWorklist.addAll(Arrays.asList(iface.getInterfaces()));
+ }
+
+ return true;
+ }
+
+ public JavaType lookupAllowUnresolved(JavaType type) {
+ if (!(type instanceof ResolvedJavaType resolvedType)) {
+ return type;
+ }
+ return lookup(resolvedType);
+ }
+
+ public List lookup(List extends JavaType> inputTypes) {
+ if (inputTypes == null) {
+ return null;
+ }
+ return inputTypes.stream().map(this::lookupAllowUnresolved).toList();
+ }
+
+ public ResolvedJavaType[] lookup(ResolvedJavaType[] inputTypes) {
+ if (inputTypes == null) {
+ return null;
+ }
+ ResolvedJavaType[] result = new ResolvedJavaType[inputTypes.length];
+ for (int i = 0; i < inputTypes.length; i++) {
+ result[i] = lookup(inputTypes[i]);
+ }
+ return result;
+ }
+
+ public PredictableBuildTimeInitializationType lookup(Class> clazz) {
+ return lookup(wrappedMetaAccess.lookupJavaType(clazz));
+ }
+
+ public PredictableBuildTimeInitializationField lookup(ResolvedJavaField field) {
+ if (field == null) {
+ return null;
+ }
+ if (field instanceof PredictableBuildTimeInitializationField pbtiField) {
+ assert pbtiField.getLookupLayer() == this : pbtiField;
+ return pbtiField;
+ }
+
+ ResolvedJavaField originalField = OriginalFieldProvider.getOriginalField(field);
+ ResolvedJavaField substitutedField = substitutions.lookup(originalField);
+ return fields.computeIfAbsent(originalField, _ -> new PredictableBuildTimeInitializationField(this, substitutedField));
+ }
+
+ public ResolvedJavaField[] lookup(ResolvedJavaField[] inputFields) {
+ if (inputFields == null) {
+ return null;
+ }
+ ResolvedJavaField[] result = new ResolvedJavaField[inputFields.length];
+ for (int i = 0; i < inputFields.length; i++) {
+ result[i] = lookup(inputFields[i]);
+ }
+ return result;
+ }
+
+ public PredictableBuildTimeInitializationField lookup(Field field) {
+ return lookup(wrappedMetaAccess.lookupJavaField(field));
+ }
+
+ public PredictableBuildTimeInitializationType objectType() {
+ return objectType;
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java
new file mode 100644
index 0000000000000..f74b8d1bb4c2f
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/pbti/PredictableBuildTimeInitializationType.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.hosted.classinitialization.pbti;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException;
+import com.oracle.svm.util.InterceptedFieldType;
+import com.oracle.graal.pointsto.infrastructure.WrappedJavaType;
+import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport;
+import com.oracle.svm.hosted.classinitialization.InitKind;
+import com.oracle.svm.util.OriginalClassProvider;
+
+import jdk.vm.ci.meta.Assumptions.AssumptionResult;
+import jdk.vm.ci.meta.JavaConstant;
+import jdk.vm.ci.meta.JavaKind;
+import jdk.vm.ci.meta.JavaType;
+import jdk.vm.ci.meta.ResolvedJavaField;
+import jdk.vm.ci.meta.ResolvedJavaMethod;
+import jdk.vm.ci.meta.ResolvedJavaRecordComponent;
+import jdk.vm.ci.meta.ResolvedJavaType;
+import jdk.vm.ci.meta.Signature;
+import jdk.vm.ci.meta.UnresolvedJavaField;
+import jdk.vm.ci.meta.UnresolvedJavaType;
+
+/**
+ * Type after {@link com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor substitution}.
+ * This is intentionally separate from {@link com.oracle.graal.pointsto.meta.AnalysisType} to
+ * prevent type and field lookups from expanding the analysis/hosted universe.
+ *
+ *
Implements {@link InterceptedFieldType shared interface} to access fields via the {@link com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport}
+ */
+public final class PredictableBuildTimeInitializationType implements ResolvedJavaType, WrappedJavaType, OriginalClassProvider, InterceptedFieldType {
+
+ private final PredictableBuildTimeInitializationLookupLayer lookupLayer;
+ private final ResolvedJavaType wrapped;
+ private final JavaKind storageKind;
+
+ private final ResolvedJavaType superType;
+ private PredictableBuildTimeInitializationField[] instanceObjectFields;
+ private PredictableBuildTimeInitializationField[] staticObjectFields;
+ private final Class> javaClass;
+ private final String qualifiedName;
+
+ PredictableBuildTimeInitializationType(PredictableBuildTimeInitializationLookupLayer lookupLayer, ResolvedJavaType wrapped, JavaKind storageKind) {
+ this.lookupLayer = lookupLayer;
+ this.wrapped = wrapped;
+
+ this.superType = lookupLayer.lookup(wrapped.getSuperclass());
+ this.javaClass = OriginalClassProvider.getJavaClass(this);
+ this.qualifiedName = wrapped.toJavaName();
+
+ this.storageKind = storageKind;
+ }
+
+ @Override
+ public ResolvedJavaType getWrapped() {
+ return wrapped;
+ }
+
+ @Override
+ public ResolvedJavaType unwrapTowardsOriginalType() {
+ return wrapped;
+ }
+
+ @Override
+ public String getName() {
+ return wrapped.getName();
+ }
+
+ @Override
+ public String toJavaName() {
+ return qualifiedName;
+ }
+
+ @Override
+ public String toJavaName(boolean qualified) {
+ /* Only qualified is cached, unqualified is not expected. */
+ return qualified ? qualifiedName : wrapped.toJavaName(false);
+ }
+
+ @Override
+ public boolean hasFinalizer() {
+ return wrapped.hasFinalizer();
+ }
+
+ @Override
+ public AssumptionResult hasFinalizableSubclass() {
+ return wrapped.hasFinalizableSubclass();
+ }
+
+ @Override
+ public boolean isInterface() {
+ return wrapped.isInterface();
+ }
+
+ @Override
+ public boolean isInstanceClass() {
+ return wrapped.isInstanceClass();
+ }
+
+ @Override
+ public boolean isPrimitive() {
+ return wrapped.isPrimitive();
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return wrapped.isLeaf();
+ }
+
+ @Override
+ public boolean isEnum() {
+ return wrapped.isEnum();
+ }
+
+ @Override
+ public boolean isRecord() {
+ return wrapped.isRecord();
+ }
+
+ @Override
+ public List extends ResolvedJavaRecordComponent> getRecordComponents() {
+ return wrapped.getRecordComponents();
+ }
+
+ @Override
+ public boolean isInitialized() {
+ InitKind kind = ClassInitializationSupport.singleton().computedInitKindFor(javaClass);
+ /* Check that when isInitialized is called, class is already initialized. */
+ assert kind != null;
+ return kind == InitKind.BUILD_TIME;
+ }
+
+ @Override
+ public void initialize() {
+ assert ClassInitializationSupport.singleton().maybeInitializeAtBuildTime(this);
+ }
+
+ @Override
+ public boolean isLinked() {
+ return wrapped.isLinked();
+ }
+
+ @Override
+ public void link() {
+ wrapped.link();
+ }
+
+ @Override
+ public boolean hasDefaultMethods() {
+ return wrapped.hasDefaultMethods();
+ }
+
+ @Override
+ public boolean declaresDefaultMethods() {
+ return wrapped.declaresDefaultMethods();
+ }
+
+ @Override
+ public boolean isAssignableFrom(ResolvedJavaType other) {
+ return wrapped.isAssignableFrom(OriginalClassProvider.getOriginalType(other));
+ }
+
+ @Override
+ public boolean isJavaLangObject() {
+ return wrapped.isJavaLangObject();
+ }
+
+ @Override
+ public boolean isInstance(JavaConstant obj) {
+ return wrapped.isInstance(obj);
+ }
+
+ @Override
+ public ResolvedJavaType getSuperclass() {
+ return superType;
+ }
+
+ @Override
+ public ResolvedJavaType[] getInterfaces() {
+ return lookupLayer.lookup(wrapped.getInterfaces());
+ }
+
+ @Override
+ public ResolvedJavaType[] getDeclaredTypes() {
+ return lookupLayer.lookup(wrapped.getDeclaredTypes());
+ }
+
+ @Override
+ public ResolvedJavaType getSingleImplementor() {
+ return lookupLayer.lookup(wrapped.getSingleImplementor());
+ }
+
+ @Override
+ public ResolvedJavaType findLeastCommonAncestor(ResolvedJavaType otherType) {
+ return lookupLayer.lookup(wrapped.findLeastCommonAncestor(OriginalClassProvider.getOriginalType(otherType)));
+ }
+
+ @Override
+ public AssumptionResult findLeafConcreteSubtype() {
+ AssumptionResult wrappedResult = wrapped.findLeafConcreteSubtype();
+ if (wrappedResult != null && wrappedResult.isAssumptionFree()) {
+ return new AssumptionResult<>(lookupLayer.lookup(wrappedResult.getResult()));
+ }
+ return null;
+ }
+
+ @Override
+ public ResolvedJavaType getComponentType() {
+ return lookupLayer.lookup(wrapped.getComponentType());
+ }
+
+ @Override
+ public ResolvedJavaType getElementalType() {
+ return lookupLayer.lookup(wrapped.getElementalType());
+ }
+
+ @Override
+ public ResolvedJavaType getArrayClass() {
+ return lookupLayer.lookup(wrapped.getArrayClass());
+ }
+
+ @Override
+ public boolean isHidden() {
+ return wrapped.isHidden();
+ }
+
+ @Override
+ public List extends JavaType> getPermittedSubclasses() {
+ return lookupLayer.lookup(wrapped.getPermittedSubclasses());
+ }
+
+ @Override
+ public ResolvedJavaMethod resolveMethod(ResolvedJavaMethod method, ResolvedJavaType callerType) {
+ return wrapped.resolveMethod(method, callerType);
+ }
+
+ @Override
+ public ResolvedJavaMethod resolveConcreteMethod(ResolvedJavaMethod method, ResolvedJavaType callerType) {
+ return wrapped.resolveConcreteMethod(method, callerType);
+ }
+
+ @Override
+ public AssumptionResult findUniqueConcreteMethod(ResolvedJavaMethod method) {
+ return wrapped.findUniqueConcreteMethod(method);
+ }
+
+ @Override
+ public PredictableBuildTimeInitializationField[] getInstanceFields(boolean includeSuperclasses) {
+ assert includeSuperclasses; /* There is no need to filter these out */
+ if (instanceObjectFields == null) {
+ instanceObjectFields = convertFields(wrapped.getInstanceFields(true));
+ }
+ return instanceObjectFields;
+ }
+
+ @Override
+ public PredictableBuildTimeInitializationField[] getStaticFields() {
+ if (staticObjectFields == null) {
+ staticObjectFields = convertFields(wrapped.getStaticFields());
+ }
+ return staticObjectFields;
+ }
+
+ /**
+ * Similar implementation to {@code com.oracle.graal.pointsto.meta.AnalysisType#convertFields}.
+ */
+ private PredictableBuildTimeInitializationField[] convertFields(ResolvedJavaField[] rawFields) {
+ PredictableBuildTimeInitializationField[] result = new PredictableBuildTimeInitializationField[rawFields.length];
+
+ int index = 0;
+ for (ResolvedJavaField rawField : rawFields) {
+ if (!rawField.isInternal() && lookupLayer.hostVM().platformSupported(rawField)) {
+ try {
+ PredictableBuildTimeInitializationField field = lookupLayer.lookup(rawField);
+ if (field != null && field.getStorageKind().isObject()) {
+ /* PBTI only cares about objects */
+ result[index++] = field;
+ }
+ } catch (UnsupportedFeatureException ex) {
+ // Ignore deleted fields and fields of deleted types.
+ }
+ }
+ }
+ return index == result.length ? result : Arrays.copyOf(result, index);
+ }
+
+ @Override
+ public ResolvedJavaField findInstanceFieldWithOffset(long offset, JavaKind expectedKind) {
+ ResolvedJavaField field = wrapped.findInstanceFieldWithOffset(offset, expectedKind);
+ return lookupLayer.lookup(field);
+ }
+
+ @Override
+ public String getSourceFileName() {
+ return wrapped.getSourceFileName();
+ }
+
+ @Override
+ public boolean isLocal() {
+ return wrapped.isLocal();
+ }
+
+ @Override
+ public boolean isMember() {
+ return wrapped.isMember();
+ }
+
+ @Override
+ public ResolvedJavaType getEnclosingType() {
+ return lookupLayer.lookup(wrapped.getEnclosingType());
+ }
+
+ @Override
+ public ResolvedJavaMethod getEnclosingMethod() {
+ return wrapped.getEnclosingMethod();
+ }
+
+ @Override
+ public ResolvedJavaMethod[] getDeclaredConstructors() {
+ return wrapped.getDeclaredConstructors();
+ }
+
+ @Override
+ public ResolvedJavaMethod[] getDeclaredConstructors(boolean forceLink) {
+ return wrapped.getDeclaredConstructors(forceLink);
+ }
+
+ @Override
+ public ResolvedJavaMethod[] getDeclaredMethods() {
+ return wrapped.getDeclaredMethods();
+ }
+
+ @Override
+ public ResolvedJavaMethod[] getDeclaredMethods(boolean forceLink) {
+ return wrapped.getDeclaredMethods(forceLink);
+ }
+
+ @Override
+ public List getAllMethods(boolean forceLink) {
+ return wrapped.getAllMethods(forceLink);
+ }
+
+ @Override
+ public ResolvedJavaMethod getClassInitializer() {
+ return wrapped.getClassInitializer();
+ }
+
+ @Override
+ public ResolvedJavaMethod findMethod(String name, Signature signature) {
+ return wrapped.findMethod(name, signature);
+ }
+
+ @Override
+ public boolean isCloneableWithAllocation() {
+ return wrapped.isCloneableWithAllocation();
+ }
+
+ @Override
+ public ResolvedJavaType lookupType(UnresolvedJavaType unresolvedJavaType, boolean resolve) {
+ return lookupLayer.lookup(wrapped.lookupType(unresolvedJavaType, resolve));
+ }
+
+ @Override
+ public ResolvedJavaField resolveField(UnresolvedJavaField unresolvedJavaField, ResolvedJavaType accessingClass) {
+ return wrapped.resolveField(unresolvedJavaField, accessingClass);
+ }
+
+ @Override
+ public boolean isArray() {
+ return wrapped.isArray();
+ }
+
+ @Override
+ public JavaKind getJavaKind() {
+ return wrapped.getJavaKind();
+ }
+
+ @Override
+ public JavaKind getStorageKind() {
+ return storageKind;
+ }
+
+ @Override
+ public ResolvedJavaType resolve(ResolvedJavaType accessingClass) {
+ return this;
+ }
+
+ @Override
+ public int getModifiers() {
+ return wrapped.getModifiers();
+ }
+
+ @Override
+ public boolean isSynchronized() {
+ return wrapped.isSynchronized();
+ }
+
+ @Override
+ public boolean isStatic() {
+ return wrapped.isStatic();
+ }
+
+ @Override
+ public boolean isFinalFlagSet() {
+ return wrapped.isFinalFlagSet();
+ }
+
+ @Override
+ public boolean isPublic() {
+ return wrapped.isPublic();
+ }
+
+ @Override
+ public boolean isPackagePrivate() {
+ return wrapped.isPackagePrivate();
+ }
+
+ @Override
+ public boolean isPrivate() {
+ return wrapped.isPrivate();
+ }
+
+ @Override
+ public boolean isProtected() {
+ return wrapped.isProtected();
+ }
+
+ @Override
+ public boolean isTransient() {
+ return wrapped.isTransient();
+ }
+
+ @Override
+ public boolean isStrict() {
+ return wrapped.isStrict();
+ }
+
+ @Override
+ public boolean isVolatile() {
+ return wrapped.isVolatile();
+ }
+
+ @Override
+ public boolean isNative() {
+ return wrapped.isNative();
+ }
+
+ @Override
+ public boolean isAbstract() {
+ return wrapped.isAbstract();
+ }
+
+ @Override
+ public boolean isConcrete() {
+ return wrapped.isConcrete();
+ }
+
+ @Override
+ public String toString() {
+ return "PBTIType<" + wrapped.toJavaName(true) + ">";
+ }
+
+ @Override
+ public int hashCode() {
+ return wrapped.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PredictableBuildTimeInitializationType pbtiType) {
+ return pbtiType.wrapped == wrapped;
+ }
+ return false;
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java
index 0dedb593f0373..b658416079e26 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CEntryPointLiteralFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -27,7 +27,6 @@
import static com.oracle.svm.shared.util.VMError.shouldNotReachHere;
import java.lang.reflect.Method;
-import java.util.function.Function;
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.function.CEntryPointLiteral;
@@ -46,6 +45,7 @@
import com.oracle.svm.hosted.meta.PatchedWordConstant;
import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
import com.oracle.svm.util.GuestAnnotationAccess;
+import com.oracle.svm.util.HeapObjectConstantReplacer;
import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.ResolvedJavaMethod;
@@ -53,7 +53,7 @@
@AutomaticallyRegisteredFeature
public class CEntryPointLiteralFeature implements InternalFeature {
- class CEntryPointLiteralObjectReplacer implements Function {
+ class CEntryPointLiteralObjectReplacer extends HeapObjectConstantReplacer {
private final HostedValuesProvider hostedValuesProvider;
/** Creates a replacer that canonicalizes changed word values with {@code hostedValuesProvider}. */
@@ -62,7 +62,7 @@ class CEntryPointLiteralObjectReplacer implements Function> allMaps = ConcurrentHashMap.newKeySet();
private final Set> allLists = ConcurrentHashMap.newKeySet();
private final ScanReason scanReason = new OtherReason("Manual rescan triggered from " + ImageHeapCollectionSupport.class);
+ final Replacer replacer = new Replacer();
static ImageHeapCollectionSupport singleton() {
return ImageSingletons.lookup(ImageHeapCollectionSupport.class);
}
- /**
- * Rewrites hosted image-heap collections to their runtime storage objects and records the
- * hosted collections that need later update propagation. During analysis, first reachability of
- * a collection registers it for subsequent update/rescan handling; after analysis, encountering
- * a collection that was never captured is treated as an invariant violation.
- */
- Object replaceHostedWithRuntime(Object obj) {
- if (obj instanceof HostedImageHeapMap) {
- HostedImageHeapMap, ?> hostedImageHeapMap = (HostedImageHeapMap, ?>) obj;
- if (BuildPhaseProvider.isAnalysisFinished()) {
- VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis");
- } else {
- allMaps.add(hostedImageHeapMap);
- if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap layeredImageHeapMap) {
- LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap);
+ final class Replacer extends HeapObjectReplacer {
+ /**
+ * Rewrites hosted image-heap collections to their runtime storage objects and records the
+ * hosted collections that need later update propagation. During analysis, first reachability of
+ * a collection registers it for subsequent update/rescan handling; after analysis, encountering
+ * a collection that was never captured is treated as an invariant violation.
+ */
+ @Override
+ public Object replaceObject(Object obj, boolean reachable) {
+ if (obj instanceof HostedImageHeapMap) {
+ HostedImageHeapMap, ?> hostedImageHeapMap = (HostedImageHeapMap, ?>) obj;
+ if (reachable) {
+ if (BuildPhaseProvider.isAnalysisFinished()) {
+ VMError.guarantee(allMaps.contains(hostedImageHeapMap), "ImageHeapMap reachable after analysis that was not seen during analysis");
+ } else {
+ allMaps.add(hostedImageHeapMap);
+ if (hostedImageHeapMap.getRuntimeMap() instanceof LayeredImageHeapMap layeredImageHeapMap) {
+ LayeredHostedImageHeapMapCollector.singleton().registerReachableHostedImageHeapMap(layeredImageHeapMap);
+ }
+ }
}
+ return hostedImageHeapMap.getRuntimeMap();
+ } else if (obj instanceof HostedImageHeapList> hostedImageHeapList) {
+ if (reachable) {
+ if (BuildPhaseProvider.isAnalysisFinished()) {
+ VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis");
+ } else {
+ allLists.add(hostedImageHeapList);
+ }
+ }
+ return hostedImageHeapList.getRuntimeList();
}
- return hostedImageHeapMap.getRuntimeMap();
- } else if (obj instanceof HostedImageHeapList> hostedImageHeapList) {
- if (BuildPhaseProvider.isAnalysisFinished()) {
- VMError.guarantee(allLists.contains(hostedImageHeapList), "ImageHeapList reachable after analysis that was not seen during analysis");
- } else {
- allLists.add(hostedImageHeapList);
- }
- return hostedImageHeapList.getRuntimeList();
+ return obj;
}
- return obj;
}
/**
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java
index a9971faa63d5a..d1a256b362189 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/image/sources/SourceCache.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2020, Red Hat Inc. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
@@ -40,6 +40,8 @@
import java.util.List;
import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
import com.oracle.svm.core.SubstrateOptions;
import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
@@ -62,8 +64,17 @@
* the local cache or cached when not already present. Subclasses are responsible for providing
* behaviours that identify an original source for addition to the cache and for verifying that a
* cached file is not out of date with respect to its original.
+ *
+ *
+ * Marked as HOSTED_ONLY so
+ * {@link com.oracle.svm.hosted.classinitialization.pbti.PredictableBuildTimeInitializationFeature}
+ * does not mark it as a failure that can appear in the generated image. In particular, the
+ * SourceRoots can contain {@code sun.nio.ch.FileChannelImpl}, which is initialized at run-time, and
+ * available from {@code SourceCache$SourceRoots.srcRoots.elementData[0].path.zfs.ch}, held
+ * by {@code jdk.nio.zipfs.ZipFileSystem}.
+ *
*/
-
+@Platforms(Platform.HOSTED_ONLY.class)
public class SourceCache {
private static final class SourceRoots {
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java
index 9ccd5c709ad7d..8cee79657b54d 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/HostedClassLoaderPackageManagement.java
@@ -36,7 +36,6 @@
import org.graalvm.nativeimage.ImageSingletons;
-import com.oracle.graal.pointsto.heap.ImageHeapConstant;
import com.oracle.svm.shared.BuildPhaseProvider;
import com.oracle.svm.shared.singletons.AutomaticallyRegisteredImageSingleton;
import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport;
@@ -56,10 +55,12 @@
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
import com.oracle.svm.shared.util.VMError;
import com.oracle.svm.shared.util.ReflectionUtil;
+import com.oracle.svm.util.HeapConstantToConstantReplacer;
import jdk.graal.compiler.debug.Assertions;
import jdk.graal.compiler.debug.GraalError;
import jdk.internal.loader.ClassLoaders;
+import jdk.vm.ci.meta.JavaConstant;
/**
* This class stores information about which packages need to be stored within each ClassLoader.
@@ -229,8 +230,11 @@ public void registerPackage(ClassLoader runtimeClassLoader, String packageName,
}
}
- public ImageHeapConstant replaceWithPriorLayerPackage(Object obj) {
+ public JavaConstant replaceWithPriorLayerPackage(Object obj, boolean reachable) {
if (obj instanceof Package hostedPackage) {
+ if (!reachable) {
+ return HeapConstantToConstantReplacer.CANNOT_REPLACE_MARKER;
+ }
/*
* Determine the package's ClassLoader
*/
@@ -240,7 +244,7 @@ public ImageHeapConstant replaceWithPriorLayerPackage(Object obj) {
var keyName = generateKeyName(hostedPackage.getName());
if (registry.constantExists(keyName)) {
- return (ImageHeapConstant) registry.getConstant(keyName);
+ return registry.getConstant(keyName);
}
}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java
index 3fcc4a775bd87..be2f181e6a4bd 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JDKInitializationFeature.java
@@ -93,6 +93,10 @@ public void afterRegistration(AfterRegistrationAccess access) {
if (FutureDefaultsOptions.resourceBundlesInitializedAtRunTime()) {
rci.initializeAtRunTime("java.text.BreakIterator", FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON);
rci.initializeAtRunTime("java.text.BreakIterator$BreakIteratorCache", FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON);
+
+ /* PBTI will find its static fields when resource bundles are initialized at run-time */
+ rci.initializeAtRunTime("com.oracle.svm.core.jdk.localization.substitutions.Util_java_text_BreakIterator",
+ FutureDefaultsOptions.RUN_TIME_INITIALIZE_RESOURCE_BUNDLES_REASON);
}
rci.initializeAtBuildTime("java.time", JDK_CLASS_REASON);
// see HijrahChronologyFeature for more details
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java
index 8e143984dd25e..a65f489822bbb 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventFeature.java
@@ -33,8 +33,8 @@
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
-import com.oracle.svm.shared.BuildPhaseProvider;
-import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
+import com.oracle.graal.pointsto.ObjectScanner;
+import com.oracle.graal.pointsto.heap.ImageHeapScanner;
import com.oracle.svm.core.feature.InternalFeature;
import com.oracle.svm.core.fieldvaluetransformer.JVMCIFieldValueTransformerWithAvailability;
import com.oracle.svm.core.hub.DynamicHub;
@@ -45,18 +45,26 @@
import com.oracle.svm.core.jfr.JfrJavaEvents;
import com.oracle.svm.core.jfr.traceid.JfrTraceId;
import com.oracle.svm.core.jfr.traceid.JfrTraceIdMap;
-import com.oracle.svm.jvmci.shared.meta.SharedType;
import com.oracle.svm.hosted.FeatureImpl;
import com.oracle.svm.hosted.GuestTypes;
import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport;
import com.oracle.svm.hosted.reflect.ReflectionFeature;
+import com.oracle.svm.jvmci.shared.meta.SharedType;
+import com.oracle.svm.shared.BuildPhaseProvider;
+import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature;
+import com.oracle.svm.shared.util.VMError;
import com.oracle.svm.util.GuestAccess;
import com.oracle.svm.util.JVMCIReflectionUtil;
import com.oracle.svm.util.JVMCIRuntimeClassInitializationSupport;
+import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.svm.util.dynamicaccess.JVMCIRuntimeReflection;
import jdk.internal.event.Event;
import jdk.jfr.internal.JVM;
+import jdk.jfr.internal.JVMSupport;
+import jdk.jfr.internal.MetadataRepository;
+import jdk.jfr.internal.TypeLibrary;
+import jdk.jfr.internal.event.EventConfiguration;
import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaType;
@@ -68,6 +76,8 @@
*/
@AutomaticallyRegisteredFeature
public class JfrEventFeature implements InternalFeature {
+ private JfrEventSubstitution eventSubstitution;
+
@Override
public boolean isInConfiguration(IsInConfigurationAccess access) {
return JfrFeature.isInConfiguration(false);
@@ -97,7 +107,9 @@ public void duringSetup(DuringSetupAccess c) {
String reason = "from feature " + JfrEventFeature.class + " with '" + eventSubClass.toJavaName(false) + "'";
JVMCIRuntimeClassInitializationSupport.singleton().initializeAtBuildTime(eventSubClass, reason);
}
- config.registerSubstitutionProcessor(new JfrEventSubstitution(metaAccess, config.getUniverse().getHeapScanner()));
+
+ eventSubstitution = new JfrEventSubstitution(metaAccess);
+ config.registerSubstitutionProcessor(eventSubstitution);
/*
* The value of this field is set later in the beforeCompilation method after analysis
@@ -116,6 +128,53 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue)
return originalValue;
}
});
+
+ ImageHeapScanner heapScanner = config.getUniverse().getHeapScanner();
+ config.registerOnTypeCreatedCallback(type -> {
+ if (!eventSubstitution.needsClassRedefinition(type.getWrapped())) {
+ return;
+ }
+ VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished());
+
+ Class extends Event> eventClass = OriginalClassProvider.getJavaClass(type).asSubclass(Event.class);
+
+ EventConfiguration configuration = eventSubstitution.takeEventConfiguration(eventClass);
+ if (configuration != null) {
+ /*
+ * Set the previously created configuration back for this event class.
+ * This process allows JfrEventSubstitution be used outside the analysis,
+ * without affecting the image heap with extra configuration and type objects.
+ * These objects are only used, if an analysis type is created for said event class.
+ */
+ JVMSupport.setConfiguration(eventClass, configuration);
+ MetadataRepository.getInstance().register(eventClass);
+ }
+
+ /*
+ * The rest of the code was previously present in JfrEventSubstitution,
+ * but also required the analysis to be used for heap scanning.
+ * Now, registration and heap scanning is only performed after AnalysisType is created.
+ */
+ JfrJavaEvents.registerEventClass(eventClass);
+
+ configuration = JVMSupport.getConfiguration(eventClass);
+ if (!configuration.isRegistered()) {
+ /* Keep previous behavior, register events that are not registered*/
+ configuration.platformEventType().setRegistered(true);
+ }
+
+ ObjectScanner.ScanReason reason = new ObjectScanner.OtherReason("Manual rescan for " + eventClass + " triggered from " + JfrEventFeature.class);
+ heapScanner.rescanObject(configuration, reason);
+ });
+ }
+
+ @Override
+ public void afterAnalysis(AfterAnalysisAccess access) {
+ /* Prune unregistered types that were added from
+ * the substitution processor, but did not access onTypeCreated part.
+ * This can for example be custom metadata type.
+ */
+ TypeLibrary.clearUnregistered();
}
@Override
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java
index 9fcc2ce2e104a..f74cae44e2f02 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jfr/JfrEventSubstitution.java
@@ -29,17 +29,15 @@
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
-import com.oracle.graal.pointsto.ObjectScanner.OtherReason;
-import com.oracle.graal.pointsto.ObjectScanner.ScanReason;
-import com.oracle.graal.pointsto.heap.ImageHeapScanner;
-import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor;
-import com.oracle.svm.shared.BuildPhaseProvider;
-import com.oracle.svm.core.jfr.JfrJavaEvents;
import com.oracle.svm.shared.util.VMError;
+import com.oracle.svm.util.OriginalClassProvider;
import jdk.jfr.internal.JVM;
+import jdk.jfr.internal.JVMSupport;
import jdk.jfr.internal.MetadataRepository;
+import jdk.jfr.internal.TypeLibrary;
+import jdk.jfr.internal.event.EventConfiguration;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaMethod;
@@ -52,20 +50,24 @@
*/
@Platforms(Platform.HOSTED_ONLY.class)
public class JfrEventSubstitution extends SubstitutionProcessor {
-
- private final ImageHeapScanner heapScanner;
-
private final ResolvedJavaType baseEventType;
private final ConcurrentHashMap typeSubstitution;
private final ConcurrentHashMap methodSubstitutions;
private final ConcurrentHashMap fieldSubstitutions;
- JfrEventSubstitution(MetaAccessProvider metaAccess, ImageHeapScanner heapScanner) {
- this.heapScanner = heapScanner;
+ /**
+ * Event configurations stored for events that do not yet have
+ * the associated analysis type. {@link #takeEventConfiguration(Class)}
+ * is called in {@link JfrEventFeature} when analysis type is created.
+ */
+ private final ConcurrentHashMap, EventConfiguration> eventConfigurations;
+
+ JfrEventSubstitution(MetaAccessProvider metaAccess) {
baseEventType = metaAccess.lookupJavaType(jdk.internal.event.Event.class);
typeSubstitution = new ConcurrentHashMap<>();
methodSubstitutions = new ConcurrentHashMap<>();
fieldSubstitutions = new ConcurrentHashMap<>();
+ eventConfigurations = new ConcurrentHashMap<>();
}
@Override
@@ -138,21 +140,52 @@ private static ResolvedJavaMethod initEventMethod(ResolvedJavaMethod oldMethod)
}
private Boolean initEventClass(ResolvedJavaType eventType) throws RuntimeException {
- VMError.guarantee(!BuildPhaseProvider.isAnalysisFinished());
try {
Class extends jdk.internal.event.Event> newEventClass = OriginalClassProvider.getJavaClass(eventType).asSubclass(jdk.internal.event.Event.class);
eventType.initialize();
- MetadataRepository.getInstance().register(newEventClass);
+ MetadataRepository repository = MetadataRepository.getInstance();
- JfrJavaEvents.registerEventClass(newEventClass);
- // the reflection registration for the event handler field is delayed to the JfrFeature
- // duringAnalysis callback so it does not race/interfere with other retransforms
+ EventConfiguration previousConfiguration = JVMSupport.getConfiguration(newEventClass);
+ boolean previouslyUnregistered = previousConfiguration != null && !previousConfiguration.isRegistered();
+ repository.register(newEventClass);
+
+ /*
+ * Retransformation requires the event to have a configuration, that is why register() is called.
+ * The instrumentation is required for the substitution processor, but instrumented methods
+ * reference the configuration that is cleared until AnalysisType is created to reduce number configurations
+ * included in the image heap when the processor is called outside of analysis.
+ */
JVM.retransformClasses(new Class>[]{newEventClass});
- // make sure the EventConfiguration object is fully scanned
- ScanReason reason = new OtherReason("Manual rescan for " + newEventClass + " triggered from " + JfrEventSubstitution.class);
- heapScanner.rescanObject(JVM.getConfiguration(newEventClass), reason);
+ /* JDK internal events can still be emitted by the builder VM after retransformation. */
+ if (previousConfiguration == null && jdk.jfr.Event.class.isAssignableFrom(newEventClass)) {
+ /*
+ * If the event configuration was not previously present, then the external
+ * effects are reverted - type is unregistered, removed and configuration is reset.
+ *
+ * This allows the substitution processor be used outside of analysis, but
+ * have correct field/method substitutions after instrumentation without
+ * affecting the image size with configurations for unused types.
+ * The only "side effect" of this approach, is that the class is still instrumented
+ * in the builder/host vm.
+ *
+ * When analysis type is created for this class, then configuration and type
+ * information (TypeLibrary) are re-added back in JfrEventFeature in a onTypeCreated event handler.
+ */
+ EventConfiguration configuration = JVMSupport.getConfiguration(newEventClass);
+ eventConfigurations.put(newEventClass, configuration);
+
+ repository.unregister(newEventClass.asSubclass(jdk.jfr.Event.class));
+ TypeLibrary.removeType(configuration.platformEventType().getId());
+ JVMSupport.setConfiguration(newEventClass, null);
+ } else if (previouslyUnregistered) {
+ /*
+ * Previous configuration was not registered, mark the current one as
+ * unregistered and only mark it as registered in JfrEventFeature after analysis type has been created.
+ */
+ JVMSupport.getConfiguration(newEventClass).platformEventType().setRegistered(false);
+ }
return Boolean.TRUE;
} catch (Throwable ex) {
@@ -160,7 +193,11 @@ private Boolean initEventClass(ResolvedJavaType eventType) throws RuntimeExcepti
}
}
- private boolean needsClassRedefinition(ResolvedJavaType type) {
+ EventConfiguration takeEventConfiguration(Class extends jdk.internal.event.Event> eventClass) {
+ return eventConfigurations.remove(eventClass);
+ }
+
+ boolean needsClassRedefinition(ResolvedJavaType type) {
return !type.isAbstract() && baseEventType.isAssignableFrom(type) && !baseEventType.equals(type);
}
}
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java
index 1008c046f779c..7917171493bc6 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/AutomaticUnsafeTransformationSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -38,14 +38,14 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.graalvm.collections.EconomicSet;
-import com.oracle.graal.pointsto.BigBang;
-import com.oracle.graal.pointsto.meta.AnalysisField;
import com.oracle.graal.pointsto.phases.NoClassInitializationPlugin;
+import com.oracle.graal.pointsto.util.AnalysisFuture;
import com.oracle.svm.core.ParsingReason;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
@@ -110,6 +110,14 @@
* scale and array index shift unsafe computations.
*/
public class AutomaticUnsafeTransformationSupport {
+ /**
+ * Field that was accessed by the initializer.
+ *
+ * @param targetField Field that was unsafely accessed during initialization
+ * @param reason Field that stores result of the unsafe access
+ */
+ public record AccessedField(ResolvedJavaField targetField, ResolvedJavaField reason) {
+ }
private static final int BASIC_LEVEL = 1;
private static final int INFO_LEVEL = 2;
@@ -139,6 +147,12 @@ static class Options {
private final EconomicSet neverInlineSet = EconomicSet.create();
private final EconomicSet noCheckedExceptionsSet = EconomicSet.create();
+ /**
+ * Map of unsafe transformations being computed asynchronously.
+ * The result is a list of fields that were accessed unsafely.
+ */
+ private final ConcurrentHashMap>> transformationTasks = new ConcurrentHashMap<>();
+
private final Plugins plugins;
private final OptionValues options;
@@ -268,10 +282,25 @@ public AutomaticUnsafeTransformationSupport(OptionValues options, AnnotationSubs
suppressWarnings = List.of(originalMetaAccess.lookupJavaType(ReflectionUtil.lookupClass(false, "sun.security.provider.ByteArrayAccess")));
}
- public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType hostType) {
+ /**
+ * Returns the task that installs automatic field value transformers for {@code hostType}.
+ *
+ * @return Fields that were unsafely accessed by the initializer.
+ */
+ public AnalysisFuture> getOrComputeTransformations(SVMHost hostVM, ResolvedJavaType hostType) {
+ return transformationTasks.computeIfAbsent(hostType,
+ type -> new AnalysisFuture<>(() -> computeTransformations(hostVM, type)));
+ }
+
+ /**
+ * Computes field transformations based on the initializer and returns a list of
+ * fields that were unsafely accessed.
+ */
+ private List computeTransformations(SVMHost hostVM, ResolvedJavaType hostType) {
if (hostType.isArray()) {
- return;
+ return List.of();
}
+
if (!hostVM.getClassInitializationSupport().maybeInitializeAtBuildTime(hostType)) {
/*
* The class initializer of this type is executed at run time. The methods in Unsafe are
@@ -285,15 +314,17 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType
* perform the unsafe access registration. And if not, the user needs to provide a
* proper manual configuration file.
*/
- return;
+ return List.of();
}
if (annotationSubstitutions.findFullSubstitution(hostType).isPresent()) {
/* If the class is substituted clinit will be eliminated, so bail early. */
reportSkippedTransformation(hostType);
- return;
+ return List.of();
}
+ List accessedFields = new ArrayList<>();
+
/* Detect field offset computation in static initializers. */
ResolvedJavaMethod clinit = hostType.getClassInitializer();
@@ -309,16 +340,16 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType
for (Invoke invoke : clinitGraph.getInvokes()) {
if (invoke.callTarget() instanceof MethodCallTargetNode) {
if (isInvokeTo(invoke, unsafeStaticFieldBaseMethod)) {
- processUnsafeFieldComputation(bb, hostType, invoke, StaticFieldBase);
+ processUnsafeFieldComputation(hostType, invoke, StaticFieldBase, accessedFields);
} else if (isInvokeTo(invoke, unsafeObjectFieldOffsetFieldMethod) || isInvokeTo(invoke, sunMiscUnsafeObjectFieldOffsetMethod) ||
isInvokeTo(invoke, unsafeStaticFieldOffsetMethod)) {
- processUnsafeFieldComputation(bb, hostType, invoke, FieldOffset);
+ processUnsafeFieldComputation(hostType, invoke, FieldOffset, accessedFields);
} else if (isInvokeTo(invoke, unsafeObjectFieldOffsetClassStringMethod)) {
- processUnsafeObjectFieldOffsetClassStringInvoke(bb, hostType, invoke);
+ processUnsafeObjectFieldOffsetClassStringInvoke(hostType, invoke, accessedFields);
} else if (isInvokeTo(invoke, unsafeArrayBaseOffsetMethod)) {
- processUnsafeArrayBaseOffsetInvoke(bb, hostType, invoke);
+ processUnsafeArrayBaseOffsetInvoke(hostType, invoke);
} else if (isInvokeTo(invoke, unsafeArrayIndexScaleMethod)) {
- processUnsafeArrayIndexScaleInvoke(bb, hostType, invoke, clinitGraph);
+ processUnsafeArrayIndexScaleInvoke(hostType, invoke, clinitGraph);
}
}
}
@@ -328,6 +359,7 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType
}
}
+ return accessedFields;
}
/**
@@ -341,7 +373,7 @@ public void computeTransformations(BigBang bb, SVMHost hostVM, ResolvedJavaType
*
* static final long fieldOffset = Unsafe.getUnsafe().staticFieldBase(X.class.getDeclaredField("f"));
*/
- private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, Invoke invoke, Kind kind) {
+ private void processUnsafeFieldComputation(ResolvedJavaType type, Invoke invoke, Kind kind, List accessedFields) {
List> unsuccessfulReasons = new ArrayList<>();
ResolvedJavaField targetField = null;
@@ -357,7 +389,7 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In
} else {
unsuccessfulReasons.add(() -> "The argument of " + methodFormat + " is not a constant value or a field load that can be constant-folded.");
}
- processUnsafeFieldComputation(bb, type, invoke, kind, unsuccessfulReasons, targetField);
+ processUnsafeFieldComputation(type, invoke, kind, unsuccessfulReasons, targetField, accessedFields);
}
/**
@@ -417,7 +449,7 @@ private boolean isValidField(Invoke invoke, ResolvedJavaField field, List static final long fieldOffset = Unsafe.getUnsafe().objectFieldOffset(X.class, "f");
*/
- private void processUnsafeObjectFieldOffsetClassStringInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeObjectFieldOffsetInvoke) {
+ private void processUnsafeObjectFieldOffsetClassStringInvoke(ResolvedJavaType type, Invoke unsafeObjectFieldOffsetInvoke, List accessedFields) {
List> unsuccessfulReasons = new ArrayList<>();
ResolvedJavaType targetFieldHolder = null;
@@ -452,10 +484,11 @@ private void processUnsafeObjectFieldOffsetClassStringInvoke(BigBang bb, Resolve
unsuccessfulReasons.add(() -> "The arguments of Unsafe.objectFieldOffset(Class, String) do not reference an existing field.");
}
}
- processUnsafeFieldComputation(bb, type, unsafeObjectFieldOffsetInvoke, FieldOffset, unsuccessfulReasons, targetField);
+ processUnsafeFieldComputation(type, unsafeObjectFieldOffsetInvoke, FieldOffset, unsuccessfulReasons, targetField, accessedFields);
}
- private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, Invoke invoke, Kind kind, List> unsuccessfulReasons, ResolvedJavaField targetField) {
+ private void processUnsafeFieldComputation(ResolvedJavaType type, Invoke invoke, Kind kind, List> unsuccessfulReasons, ResolvedJavaField targetField,
+ List accessedFields) {
assert kind == FieldOffset || kind == StaticFieldBase;
/*
* If the value returned by the call to Unsafe.objectFieldOffset() is stored into a field
@@ -474,7 +507,11 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In
* transformation.
*/
if (targetField != null && valueStoreField != null) {
- if (tryAutomaticTransformation(bb, valueStoreField, kind, null, targetField)) {
+ if (tryAutomaticTransformation(valueStoreField, kind, null, targetField)) {
+ if (kind == FieldOffset) {
+ accessedFields.add(new AccessedField(targetField, valueStoreField));
+ }
+
reportSuccessfulAutomaticRecomputation(kind, valueStoreField, targetField.getDeclaringClass().getName() + "." + targetField.getName());
}
} else {
@@ -488,7 +525,7 @@ private void processUnsafeFieldComputation(BigBang bb, ResolvedJavaType type, In
*
* static final long arrayBaseOffsets = Unsafe.getUnsafe().arrayBaseOffset(byte[].class);
*/
- private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayBaseOffsetInvoke) {
+ private void processUnsafeArrayBaseOffsetInvoke(ResolvedJavaType type, Invoke unsafeArrayBaseOffsetInvoke) {
List> unsuccessfulReasons = new ArrayList<>();
ResolvedJavaType arrayType = null;
@@ -509,7 +546,7 @@ private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType typ
ResolvedJavaField offsetField = result.valueStoreField;
if (arrayType != null && offsetField != null) {
ResolvedJavaType finalArrayType = arrayType;
- if (tryAutomaticTransformation(bb, offsetField, ArrayBaseOffset, finalArrayType, null)) {
+ if (tryAutomaticTransformation(offsetField, ArrayBaseOffset, finalArrayType, null)) {
reportSuccessfulAutomaticRecomputation(ArrayBaseOffset, offsetField, arrayType.toClassName());
}
} else {
@@ -526,7 +563,7 @@ private void processUnsafeArrayBaseOffsetInvoke(BigBang bb, ResolvedJavaType typ
*
* static final long byteArrayIndexScale = Unsafe.getUnsafe().arrayIndexScale(byte[].class);
*/
- private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayIndexScale, StructuredGraph clinitGraph) {
+ private void processUnsafeArrayIndexScaleInvoke(ResolvedJavaType type, Invoke unsafeArrayIndexScale, StructuredGraph clinitGraph) {
List> unsuccessfulReasons = new ArrayList<>();
ResolvedJavaType arrayType = null;
@@ -550,18 +587,18 @@ private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType typ
if (arrayType != null) {
if (indexScaleField != null) {
- if (tryAutomaticTransformation(bb, indexScaleField, ArrayIndexScale, arrayType, null)) {
+ if (tryAutomaticTransformation(indexScaleField, ArrayIndexScale, arrayType, null)) {
reportSuccessfulAutomaticRecomputation(ArrayIndexScale, indexScaleField, arrayType.toClassName());
indexScaleComputed = true;
/* Try transformation for the array index shift computation if present. */
- indexShiftComputed = processArrayIndexShiftFromField(bb, type, indexScaleField, arrayType, clinitGraph);
+ indexShiftComputed = processArrayIndexShiftFromField(type, indexScaleField, arrayType, clinitGraph);
}
} else {
/*
* The index scale is not stored into a field, it might be used to compute the index
* shift.
*/
- indexShiftComputed = processArrayIndexShiftFromLocal(bb, type, unsafeArrayIndexScale, arrayType);
+ indexShiftComputed = processArrayIndexShiftFromLocal(type, unsafeArrayIndexScale, arrayType);
}
}
if (!indexScaleComputed && !indexShiftComputed) {
@@ -591,7 +628,7 @@ private void processUnsafeArrayIndexScaleInvoke(BigBang bb, ResolvedJavaType typ
* It is important that constant folding is not enabled for the byteArrayIndexScale load because
* it would break the link between the scale and shift computations.
*/
- private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType type, ResolvedJavaField indexScaleField, ResolvedJavaType arrayType, StructuredGraph clinitGraph) {
+ private boolean processArrayIndexShiftFromField(ResolvedJavaType type, ResolvedJavaField indexScaleField, ResolvedJavaType arrayType, StructuredGraph clinitGraph) {
for (LoadFieldNode load : clinitGraph.getNodes().filter(LoadFieldNode.class)) {
if (load.field().equals(indexScaleField)) {
/*
@@ -599,7 +636,7 @@ private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType typ
* field was already found. The case where both scale and shift are computed is
* uncommon.
*/
- if (processArrayIndexShift(bb, type, arrayType, load, true)) {
+ if (processArrayIndexShift(type, arrayType, load, true)) {
/*
* Return true as soon as an index shift computed from the index scale field is
* found. It is very unlikely that there are multiple shift computations from
@@ -628,15 +665,15 @@ private boolean processArrayIndexShiftFromField(BigBang bb, ResolvedJavaType typ
* }
*
*/
- private boolean processArrayIndexShiftFromLocal(BigBang bb, ResolvedJavaType type, Invoke unsafeArrayIndexScale, ResolvedJavaType arrayType) {
+ private boolean processArrayIndexShiftFromLocal(ResolvedJavaType type, Invoke unsafeArrayIndexScale, ResolvedJavaType arrayType) {
/* Try to compute index shift. Report errors since the index scale field was not found. */
- return processArrayIndexShift(bb, type, arrayType, unsafeArrayIndexScale.asNode(), false);
+ return processArrayIndexShift(type, arrayType, unsafeArrayIndexScale.asNode(), false);
}
/**
* Try to compute the arrayIndexShift. Return true if successful, false otherwise.
*/
- private boolean processArrayIndexShift(BigBang bb, ResolvedJavaType type, ResolvedJavaType arrayType, ValueNode indexScaleValue, boolean silentFailure) {
+ private boolean processArrayIndexShift(ResolvedJavaType type, ResolvedJavaType arrayType, ValueNode indexScaleValue, boolean silentFailure) {
NodeIterable loadMethodCallTargetUsages = indexScaleValue.usages().filter(MethodCallTargetNode.class);
for (MethodCallTargetNode methodCallTarget : loadMethodCallTargetUsages) {
/* Iterate over all the calls that use the index scale value. */
@@ -669,7 +706,7 @@ private boolean processArrayIndexShift(BigBang bb, ResolvedJavaType type, Resolv
}
if (indexShiftField != null) {
- if (tryAutomaticTransformation(bb, indexShiftField, ArrayIndexShift, arrayType, null)) {
+ if (tryAutomaticTransformation(indexShiftField, ArrayIndexShift, arrayType, null)) {
reportSuccessfulAutomaticRecomputation(ArrayIndexShift, indexShiftField, arrayType.toClassName());
return true;
}
@@ -880,7 +917,7 @@ private boolean isAllowedUnsafeValueSink(Node valueNodeUsage) {
* Try to register the automatic transformation for a field. Bail if the field was deleted or a
* conflicting substitution is detected.
*/
- private boolean tryAutomaticTransformation(BigBang bb, ResolvedJavaField field, RecomputeFieldValue.Kind kind, ResolvedJavaType targetType, ResolvedJavaField targetField) {
+ private boolean tryAutomaticTransformation(ResolvedJavaField field, RecomputeFieldValue.Kind kind, ResolvedJavaType targetType, ResolvedJavaField targetField) {
if (annotationSubstitutions.isDeleted(field)) {
String conflictingSubstitution = "The field " + field.format("%H.%n") + " is marked as deleted. ";
reportConflictingSubstitution(field, kind, conflictingSubstitution);
@@ -931,19 +968,11 @@ private boolean tryAutomaticTransformation(BigBang bb, ResolvedJavaField field,
}
}
- if (kind == FieldOffset) {
- bb.postTask(_ -> toAnalysisField(bb, targetField).registerAsUnsafeAccessed(field));
- }
singleton().registerFieldValueTransformer(field, newTransformer);
return true;
}
}
- private static AnalysisField toAnalysisField(BigBang bb, ResolvedJavaField targetField) {
- VMError.guarantee(!(targetField instanceof AnalysisField), "AnalysisField not allowed: %s", targetField);
- return bb.getUniverse().lookup(targetField);
- }
-
private static void reportSkippedTransformation(ResolvedJavaType type) {
if (Options.AutomaticUnsafeTransformationLogLevel.getValue() >= DEBUG_LEVEL) {
LogUtils.warning("Skipped automatic unsafe transformation analysis for type " + type.getName() +
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java
index 76fab69267c33..78bf8ba2e1567 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/substitute/FieldValueTransformation.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -32,7 +32,6 @@
import org.graalvm.nativeimage.hosted.FieldValueTransformer;
import com.oracle.graal.pointsto.heap.TypedConstant;
-import com.oracle.graal.pointsto.meta.AnalysisField;
import com.oracle.svm.core.fieldvaluetransformer.JavaConstantWrapper;
import com.oracle.svm.core.util.UserError;
import com.oracle.svm.hosted.ameta.FieldValueInterceptionSupport;
@@ -41,6 +40,7 @@
import com.oracle.svm.util.GuestAccess;
import com.oracle.svm.util.JVMCIFieldValueTransformer;
import com.oracle.svm.util.JVMCIReflectionUtil;
+import com.oracle.svm.util.InterceptedField;
import com.oracle.svm.util.OriginalClassProvider;
import com.oracle.svm.util.OriginalFieldProvider;
@@ -72,7 +72,7 @@ public JVMCIFieldValueTransformer getFieldValueTransformer() {
return fieldValueTransformer;
}
- public JavaConstant readValue(AnalysisField field, JavaConstant receiver) {
+ public JavaConstant readValue(InterceptedField field, JavaConstant receiver) {
ReadLock readLock = valueCacheLock.readLock();
try {
readLock.lock();
@@ -107,7 +107,7 @@ public JavaConstant readValue(AnalysisField field, JavaConstant receiver) {
}
}
- private JavaConstant computeValue(AnalysisField field, JavaConstant receiver) {
+ private JavaConstant computeValue(InterceptedField field, JavaConstant receiver) {
var originalValue = fetchOriginalValue(field, receiver);
VMError.guarantee(originalValue != null, "Original value must not be `null`. Use `JavaConstant.NULL_POINTER`instead");
@@ -119,7 +119,7 @@ private JavaConstant computeValue(AnalysisField field, JavaConstant receiver) {
return result;
}
- private void checkValue(JavaConstant newValue, AnalysisField field) {
+ private void checkValue(JavaConstant newValue, InterceptedField field) {
if (newValue == null) {
throw UserError.abort("JVMCIFieldValueTransformer must not return `null`. Use `JavaConstant.NULL_POINTER`instead");
}
@@ -181,7 +181,8 @@ private JavaConstant getUnboxedConstant(JavaConstant newValue) {
return newValue;
}
- protected JavaConstant fetchOriginalValue(AnalysisField aField, JavaConstant receiver) {
+ protected JavaConstant fetchOriginalValue(InterceptedField aField, JavaConstant receiver) {
+ /* InterceptedField can either be AnalysisField or PredictableBuildTimeInitializationField */
ResolvedJavaField oField = OriginalFieldProvider.getOriginalField(aField);
if (oField == null) {
return JavaConstant.NULL_POINTER;
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java
index 9b9e2aaa318f9..7413e9adbb609 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/HostedJavaThreadsFeature.java
@@ -29,6 +29,7 @@
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
+import org.graalvm.nativeimage.hosted.FieldValueTransformerRequiresUniverse;
import com.oracle.graal.pointsto.ObjectScanner.OtherReason;
import com.oracle.graal.pointsto.ObjectScanner.ScanReason;
@@ -89,6 +90,44 @@ public JavaConstant transform(JavaConstant receiver, JavaConstant originalValue)
}
}
+ private final class NGroupsFieldValueTransformer implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse {
+ // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus
+
+ /*
+ * We must wait until reachableThreadGroups stabilizes after analysis to replace this
+ * value.
+ */
+ @Override
+ public boolean isAvailable() {
+ return BuildPhaseProvider.isHostedUniverseBuilt();
+ }
+
+ @Override
+ public Object transform(Object receiver, Object originalValue) {
+ ThreadGroup group = (ThreadGroup) receiver;
+ return reachableThreadGroups.get(group).ngroups;
+ }
+ }
+
+ private final class GroupsFieldValueTransformer implements FieldValueTransformerWithAvailability, FieldValueTransformerRequiresUniverse {
+ // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus
+
+ /*
+ * We must wait until reachableThreadGroups stabilizes after analysis to replace this
+ * value.
+ */
+ @Override
+ public boolean isAvailable() {
+ return BuildPhaseProvider.isHostedUniverseBuilt();
+ }
+
+ @Override
+ public Object transform(Object receiver, Object originalValue) {
+ ThreadGroup group = (ThreadGroup) receiver;
+ return reachableThreadGroups.get(group).groups;
+ }
+ }
+
/**
* All {@link Thread} objects that are reachable in the image heap. Only unstarted threads,
* i.e., threads in state NEW, are allowed.
@@ -118,43 +157,8 @@ public void duringSetup(DuringSetupAccess a) {
@Override
public void beforeAnalysis(BeforeAnalysisAccess a) {
- a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "ngroups"), new FieldValueTransformerWithAvailability() {
- // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus
-
- /*
- * We must wait until reachableThreadGroups stabilizes after analysis to replace this
- * value.
- */
- @Override
- public boolean isAvailable() {
- return BuildPhaseProvider.isHostedUniverseBuilt();
- }
-
- @Override
- public Object transform(Object receiver, Object originalValue) {
- ThreadGroup group = (ThreadGroup) receiver;
- return reachableThreadGroups.get(group).ngroups;
- }
- });
-
- a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "groups"), new FieldValueTransformerWithAvailability() {
- // JVMCI migration blocked by GR-72587: Migrate (virtual) thread support for terminus
-
- /*
- * We must wait until reachableThreadGroups stabilizes after analysis to replace this
- * value.
- */
- @Override
- public boolean isAvailable() {
- return BuildPhaseProvider.isHostedUniverseBuilt();
- }
-
- @Override
- public Object transform(Object receiver, Object originalValue) {
- ThreadGroup group = (ThreadGroup) receiver;
- return reachableThreadGroups.get(group).groups;
- }
- });
+ a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "ngroups"), new NGroupsFieldValueTransformer());
+ a.registerFieldValueTransformer(ReflectionUtil.lookupField(ThreadGroup.class, "groups"), new GroupsFieldValueTransformer());
FeatureImpl.BeforeAnalysisAccessImpl access = (FeatureImpl.BeforeAnalysisAccessImpl) a;
registerDeferredThreadField(access, "java.util.concurrent.locks.AbstractOwnableSynchronizer", "exclusiveOwnerThread");
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java
index 7d2bfeb221db2..402375a954b5a 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/LayeredVMThreadLocalCollector.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -81,15 +81,22 @@ private LayeredVMThreadLocalCollector(Map threadLocalAssignm
}
@Override
- public Object apply(Object source) {
- /* Make sure all names have been assigned in the prior layers. */
+ public Object replaceObject(Object source, boolean reachable) {
+ /*
+ * Make sure all names have been assigned in the prior layers
+ */
if (!initialLayer) {
if (source instanceof FastThreadLocal threadLocal) {
+ if (!reachable) {
+ /* Unreachable, current layer fast thread local can be found by pbti */
+ return CANNOT_REPLACE_MARKER;
+ }
+
var name = threadLocal.getName();
VMError.guarantee(threadLocalAssignmentMap.containsKey(name), "Found thread local which was not created in the initial layer %s", name);
}
}
- return super.apply(source);
+ return super.replaceObject(source, reachable);
}
@Override
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java
index cc61dab813da8..f2702ce359996 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/thread/VMThreadLocalCollector.java
@@ -33,7 +33,6 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
import com.oracle.svm.shared.config.ObjectLayout;
import com.oracle.svm.core.config.ObjectLayoutCompilerSupport;
@@ -47,6 +46,7 @@
import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks;
import com.oracle.svm.shared.singletons.traits.SingletonTraits;
import com.oracle.svm.shared.util.VMError;
+import com.oracle.svm.util.HeapObjectReplacer;
import jdk.graal.compiler.core.common.NumUtil;
import jdk.graal.compiler.nodes.PiNode;
@@ -59,7 +59,7 @@
* Collects all {@link FastThreadLocal} instances that are actually used by the application.
*/
@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class)
-public class VMThreadLocalCollector implements Function, VMThreadLocalOffsetProvider {
+public class VMThreadLocalCollector extends HeapObjectReplacer implements VMThreadLocalOffsetProvider {
private static final int LARGE_ENTRY_SIZE = 16;
@@ -115,8 +115,8 @@ public VMThreadLocalInfo forFastThreadLocal(FastThreadLocal threadLocal) {
}
@Override
- public Object apply(Object source) {
- if (source instanceof FastThreadLocal fastThreadLocal) {
+ public Object replaceObject(Object source, boolean reachable) {
+ if (reachable && source instanceof FastThreadLocal fastThreadLocal) {
forFastThreadLocal(fastThreadLocal);
}
/*
diff --git a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java
index 725dcdacae7e2..7bf9ac7eb463d 100644
--- a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java
+++ b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/option/FutureDefaultsOptionsSupport.java
@@ -51,4 +51,6 @@ static FutureDefaultsOptionsSupport singleton() {
boolean exactReflection();
boolean explicitFeatureSingletonRegistration();
+
+ boolean predictableBuildTimeInitialization();
}
diff --git a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java
index 667972b9f6018..313f1578c06f3 100644
--- a/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java
+++ b/substratevm/src/com.oracle.svm.shared/src/com/oracle/svm/shared/singletons/ImageSingletonsSupportImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -44,6 +44,8 @@
import org.graalvm.collections.EconomicSet;
import org.graalvm.nativeimage.ImageSingletons;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
import org.graalvm.nativeimage.impl.AnnotationExtractor;
import org.graalvm.nativeimage.impl.ImageSingletonsSupport;
@@ -298,7 +300,13 @@ static SingletonTraitMap getAnnotatedTraits(Class> singletonClass, AnnotationE
/**
* Management of the {@link ImageSingletons} registry during image generation.
+ *
+ *
+ * The {@code PredictableBuildTimeInitializationFeature} can find
+ * a violation inside this class, marking it as HOSTED_ONLY skips it.
+ *
*/
+ @Platforms(Platform.HOSTED_ONLY.class)
public static final class HostedManagement {
/**
diff --git a/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java b/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java
index b938320ba1008..ea6a884d32b83 100644
--- a/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java
+++ b/substratevm/src/com.oracle.svm.truffle/src/com/oracle/svm/truffle/api/SubstrateTruffleRuntime.java
@@ -118,7 +118,13 @@ public final class SubstrateTruffleRuntime extends OptimizedTruffleRuntime {
private static final int DEBUG_TEAR_DOWN_TIMEOUT = 2_000;
private static final int PRODUCTION_TEAR_DOWN_TIMEOUT = 10_000;
- private KnownMethods hostedCallMethods;
+ /**
+ * Can be found via com.oracle.truffle.api.Truffle.RUNTIME.hostedCallMethods
+ * in pbti and an exception thrown via {@link com.oracle.svm.graal.hosted.runtimecompilation.GraalGraphObjectReplacer}:
+ * "ResolvedJavaMethod that is not a subtype of SharedRuntimeMethod is not allowed at runtime".
+ */
+ @Platforms(Platform.HOSTED_ONLY.class) private KnownMethods hostedCallMethods;
+
private volatile BackgroundCompileQueue compileQueue;
private volatile boolean initialized;
private volatile Boolean profilingEnabled;
diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java
new file mode 100644
index 0000000000000..5d9257ec30ddf
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapConstantToConstantReplacer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.util;
+
+import java.util.function.Function;
+
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+import com.oracle.svm.shared.util.VMError;
+
+import jdk.vm.ci.meta.JavaConstant;
+
+/**
+ * A constant-to-constant replacer with restrictions on internal operations that expand the
+ * analysis/hosted universe or image heap,
+ * such as creating new universe types, methods or fields, modifying existing ones,
+ * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList}
+ * or {@code ImageHeapMap}.
+ *
+ * @param always must be ImageHeapConstant, the {@code com.oracle.svm.util} cannot access it directly.
+ */
+@Platforms(Platform.HOSTED_ONLY.class)
+public abstract class HeapConstantToConstantReplacer implements Function {
+ /**
+ * Marker returned when replacement requires a reachable object.
+ * Same as {@link HeapObjectConstantReplacer#CANNOT_REPLACE_MARKER}, because
+ * further processing can use the same condition.
+ */
+ public static final JavaConstant CANNOT_REPLACE_MARKER = HeapObjectConstantReplacer.CANNOT_REPLACE_MARKER;
+
+ @Override
+ public final T apply(JavaConstant constant) {
+ throw VMError.intentionallyUnimplemented();
+ }
+
+ /**
+ * @param constant constant to replace
+ * @param reachable whether the object is reachable and is being replaced for the image heap;
+ * when false, the internal operations described above must be avoided
+ * @return the replacement, {@code null} if this replacer does not apply, or
+ * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object
+ */
+ public abstract JavaConstant replaceConstant(JavaConstant constant, boolean reachable);
+}
diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java
new file mode 100644
index 0000000000000..d4a8058f4380b
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectConstantReplacer.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.util;
+
+import java.util.function.Function;
+
+import jdk.vm.ci.meta.JavaConstant;
+import jdk.vm.ci.meta.JavaKind;
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+import com.oracle.svm.shared.util.VMError;
+
+/**
+ * Extends functionality of object replacers to specify different
+ * replacement if the output of the replacer is meant to be stored
+ * in the image heap, or expand the analysis/hosted universe through internal operations.
+ *
+ *
+ * Expected behavior of {@link #replaceConstant(JavaConstant, boolean)} when {@code reachable} is {@code false} is
+ * that it does not expand the analysis/hosted universe or image heap through internal operations,
+ * such as creating new universe types, methods or fields, modifying existing ones,
+ * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList}
+ * or {@code ImageHeapMap}.
+ *
+ *
+ *
+ * Returns {@link #CANNOT_REPLACE_MARKER}, if {@code reachable} is false and the replacer cannot
+ * replace the object without using aforementioned internal operations.
+ *
+ */
+@Platforms(Platform.HOSTED_ONLY.class)
+public abstract class HeapObjectConstantReplacer implements Function {
+ /**
+ * This marker is used when the replacer cannot reliably replace
+ * the object without using aforementioned internal operations.
+ */
+ public static final JavaConstant CANNOT_REPLACE_MARKER = new JavaConstant() {
+ @Override
+ public JavaKind getJavaKind() {
+ return null;
+ }
+
+ @Override
+ public boolean isNull() {
+ return false;
+ }
+
+ @Override
+ public boolean isDefaultForKind() {
+ return false;
+ }
+
+ @Override
+ public Object asBoxedPrimitive() {
+ return null;
+ }
+
+ @Override
+ public int asInt() {
+ return 0;
+ }
+
+ @Override
+ public boolean asBoolean() {
+ return false;
+ }
+
+ @Override
+ public long asLong() {
+ return 0;
+ }
+
+ @Override
+ public float asFloat() {
+ return 0;
+ }
+
+ @Override
+ public double asDouble() {
+ return 0;
+ }
+ };
+
+ @Override
+ public final JavaConstant apply(JavaConstant constant) {
+ throw VMError.intentionallyUnimplemented();
+ }
+
+ /**
+ * Replace an object, optionally before it reaches the image heap. When {@code reachable} is
+ * true, this method may perform internal operations that expand the universe or image heap,
+ * such as adding entries to an {@code ImageHeapList}.
+ *
+ * @param constant that is replaced
+ * @param reachable whether the object is reachable and is being replaced for the image heap,
+ * when false, the internal operations described above must be avoided
+ * @return new object if the old one is replaced, the old object if it is unchanged, or
+ * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object
+ */
+ public abstract JavaConstant replaceConstant(JavaConstant constant, boolean reachable);
+}
diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java
new file mode 100644
index 0000000000000..23006c4191b6a
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/HeapObjectReplacer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.util;
+
+import java.util.function.Function;
+
+import org.graalvm.nativeimage.Platform;
+import org.graalvm.nativeimage.Platforms;
+
+import com.oracle.svm.shared.util.VMError;
+
+/**
+ * Extends functionality of object replacers to specify different
+ * replacement if the output of the replacer is meant to be stored
+ * in the image heap, or expand the analysis/hosted universe through internal operations.
+ *
+ *
+ * Expected behavior of {@link #replaceObject(Object, boolean)} when {@code reachable} is {@code false} is
+ * that it does not expand the analysis/hosted universe or image heap through internal operations,
+ * such as creating new universe types, methods or fields, modifying existing ones,
+ * marking constants for heap scanning, or adding new entries to an {@code ImageHeapList}
+ * or {@code ImageHeapMap}.
+ *
+ *
+ *
+ * Returns {@link #CANNOT_REPLACE_MARKER}, if {@code reachable} is false and the replacer cannot
+ * replace the object without using aforementioned mentioned internal operations.
+ *
+ */
+@Platforms(Platform.HOSTED_ONLY.class)
+public abstract class HeapObjectReplacer implements Function {
+ /**
+ * This marker is used when the replacer cannot reliably replace
+ * the object without using aforementioned internal operations.
+ */
+ public static final Object CANNOT_REPLACE_MARKER = new Object();
+
+ @Override
+ public final Object apply(Object object) {
+ throw VMError.intentionallyUnimplemented();
+ }
+
+ /**
+ * Replace an object, optionally before it reaches the image heap. When {@code reachable} is
+ * true, this method may perform internal operations that expand the universe or image heap,
+ * such as adding entries to an {@code ImageHeapList}.
+ *
+ * @param object that is replaced
+ * @param reachable whether the object is reachable and is being replaced for the image heap,
+ * when false, the internal operations described above must be avoided
+ * @return new object if the old one is replaced, the old object if it is unchanged, or
+ * {@link #CANNOT_REPLACE_MARKER} if replacement requires a reachable object
+ */
+ public abstract Object replaceObject(Object object, boolean reachable);
+}
diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java
new file mode 100644
index 0000000000000..62cfada986457
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedField.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.util;
+
+import jdk.vm.ci.meta.JavaKind;
+import jdk.vm.ci.meta.ModifiersProvider;
+import jdk.vm.ci.meta.ResolvedJavaField;
+
+/**
+ * Common methods required by the {@code FieldValueInterceptionSupport} for a field.
+ */
+public interface InterceptedField extends ResolvedJavaField, OriginalFieldProvider, ModifiersProvider {
+
+ Object getFieldValueInterceptor();
+
+ void setFieldValueInterceptor(Object fieldValueInterceptor);
+
+ void beforeFieldValueAccess();
+
+ JavaKind getStorageKind();
+
+ @Override
+ InterceptedFieldType getType();
+
+ @Override
+ InterceptedFieldType getDeclaringClass();
+}
diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java
new file mode 100644
index 0000000000000..4e870069521bf
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/InterceptedFieldType.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.util;
+
+import jdk.graal.compiler.debug.Assertions;
+import jdk.vm.ci.meta.JavaKind;
+import jdk.vm.ci.meta.ResolvedJavaType;
+import org.graalvm.word.WordBase;
+
+/**
+ * Common methods required by the {@code FieldValueInterceptionSupport} for the type of field.
+ */
+public interface InterceptedFieldType extends ResolvedJavaType, OriginalClassProvider {
+ JavaKind getStorageKind();
+
+ /**
+ * Expands the {@code isConcrete} method, while maintaining
+ * previous compatibility, taken from {@code CustomSubstitutionType}.
+ */
+ default boolean isConcreteType() {
+ return isArray() || (isInstanceClass() && !isAbstract());
+ }
+
+ /**
+ * Returns true if this type is part of the word type hierarchy, i.e, implements
+ * {@link WordBase}.
+ */
+ default boolean isWordType() {
+ /* Word types are currently the only types where kind and storageKind differ. */
+ boolean wordType = getJavaKind() != getStorageKind();
+ assert !wordType || getJavaKind().isObject() : Assertions.errorMessage("Only words are expected to have a discrepancy between java kind and storage kind", this);
+ return wordType;
+ }
+}
diff --git a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java
index d1ad86d7996d2..735b5b9d5eecd 100644
--- a/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java
+++ b/web-image/src/com.oracle.svm.webimage/src/com/oracle/svm/webimage/substitute/system/WebImageJavaLangSubstitutions.java
@@ -48,8 +48,10 @@
import com.oracle.svm.shared.NeverInline;
import com.oracle.svm.core.OS;
import com.oracle.svm.shared.util.SubstrateUtil;
+import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.AnnotateOriginal;
import com.oracle.svm.core.annotate.Delete;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.core.jdk.Resources;
@@ -291,6 +293,9 @@ private void runContinuation() {
@SuppressWarnings("unused")
final class Target_java_lang_System_Web {
+ /* Do not retain a Console instance connected to the image builder. */
+ @Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset) private static volatile Console cons;
+
@Substitute
@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
public static long currentTimeMillis() {