Java 26: Stable Values (JEP 526 — Preview)

What if you could have a lazily-initialized field that the JIT compiler treats exactly like final? JEP 526 introduces Stable Values — a new API for constants that are computed once and then permanently trusted by the JVM, enabling the same aggressive optimizations reserved today for final fields.

Status: Second Preview — requires --enable-preview. Note: the StableValue API shipped in the JDK 26 GA release; some EA builds may not include it.

The Problem with Lazy Initialization

The classic double-checked locking pattern for a lazily-initialized singleton is correct, but the JIT compiler can never fully optimize it — it must always check whether the field is initialized:

// Classic lazy singleton — correct but not JIT-optimizable as a constant
class Config {
    private static volatile Config INSTANCE;

    public static Config getInstance() {
        if (INSTANCE == null) {
            synchronized (Config.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Config();  // Expensive initialization
                }
            }
        }
        return INSTANCE;  // JIT must still load from memory on every call
    }
}

Because INSTANCE is volatile and non-final, the JIT must assume it could change at any moment. It cannot constant-fold accesses or eliminate null checks after the first non-null observation.

There’s no way to tell the JVM “this field will be set exactly once and never again — please treat it like final after that” — until now.

What StableValue Does

A StableValue<T> is a holder for a value that starts empty and can be set exactly once. The moment it is set, the JVM permanently trusts that value — enabling the same constant-folding, inlining, and escape analysis it applies to final fields:

import java.lang.invoke.StableValue;

class Config {
    // Declared once — lazily initialized, JIT-trusted after first set
    private static final StableValue<Config> INSTANCE = StableValue.of();

    public static Config getInstance() {
        return INSTANCE.orElseSet(Config::new);
        // After first call: JIT sees this as a constant
        // No locks, no volatile reads on the hot path
    }
}

The key guarantee: orElseSet is thread-safe and idempotent. If two threads race to initialize it, only one Config::new call succeeds — the other thread returns the already-set value.

Key API

StableValue<T> — Reference Types

// Create an empty stable value
StableValue<MyService> sv = StableValue.of();

// Set it (can only succeed once — returns true on first set)
boolean set = sv.trySet(new MyService());

// Get the current value (empty → throws if not yet set)
MyService svc = sv.get();

// Get or compute on first access (the most common pattern)
MyService svc = sv.orElseSet(() -> new MyService(config));

// Check whether it has been set
boolean ready = sv.isSet();

Primitive Stable Values

To avoid boxing overhead, the API includes primitive-specialized variants:

StableIntSupplier    stableInt    = StableValue.ofInt();
StableLongSupplier   stableLong   = StableValue.ofLong();
StableDoubleSupplier stableDouble = StableValue.ofDouble();

// Set and get
stableInt.trySetAsInt(42);
int val = stableInt.getAsInt();

// Or compute on first call
int val = stableInt.orElseSetAsInt(() -> computeExpensiveInt());

Stable Collections

For lazily-built maps and lists where entries are computed once per key/index:

// Stable list: element at index i is computed once by the provided function
List<Route> routes = StableValue.list(routeCount, i -> buildRoute(i));
// routes.get(3) initializes routes[3] on first access, then it's JIT-constant

// Stable map: value for each key computed once on first access
Map<String, Handler> handlers = StableValue.map(
    Set.of("/users", "/orders", "/health"),
    path -> buildHandler(path)
);

Real-World Patterns

Lazy Singleton (Framework Bootstrap)

class ApplicationContext {
    private static final StableValue<ApplicationContext> CONTEXT = StableValue.of();

    public static ApplicationContext get() {
        return CONTEXT.orElseSet(ApplicationContext::new);
    }

    private ApplicationContext() {
        // Expensive: scans classpath, loads beans, wires dependencies
        scanClasspath();
        wireBeans();
    }
}
// First call: initializes. All subsequent calls: JIT-treated as a constant.

Lazy Computed Constants

class CryptoConstants {
    // Generating an RSA key pair is expensive — do it once
    private static final StableValue<KeyPair> SIGNING_KEY = StableValue.of();

    public static KeyPair getSigningKey() {
        return SIGNING_KEY.orElseSet(() -> {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048);
            return kpg.generateKeyPair();
        });
    }
}

Per-Key Cache (Stable Map)

// Compile each regex pattern once, then cache it permanently
private static final Map<String, Pattern> PATTERNS = StableValue.map(
    Set.of("\\d+", "[a-z]+", "\\w{3,20}"),
    Pattern::compile
);

public boolean matches(String input, String regex) {
    return PATTERNS.get(regex).matcher(input).matches();
    // After first access, PATTERNS.get(regex) is JIT-constant
}

Stable Values in Records

Records can use StableValue for fields that should be computed lazily but treated as effectively final:

record Document(String content) {
    private static final StableValue<Map<String, Integer>> wordFreqCache = StableValue.of();

    // Computed once on first call — behaves like a final field thereafter
    Map<String, Integer> wordFrequencies() {
        return wordFreqCache.orElseSet(() -> computeWordFrequencies(content));
    }
}

How the JIT Optimizes Stable Values

Once a StableValue has been set, the JIT compiler can apply the same optimizations it uses for final fields:

Optimization volatile field StableValue (after set) final field
Constant folding
Null check elimination
Inline the value
Escape analysis
Memory barrier on read Required None after set None

The result: code that was written for lazy initialization gains the runtime performance of a compile-time constant — without the loss of flexibility.

StableValue vs volatile vs final

  final volatile (DCL) StableValue
JIT constant after init
Lazy initialization
Thread-safe
Set-once enforcement ✅ (compile-time) ✅ (runtime)
Primitive support ✅ (ofInt, etc.)
Memory overhead None None 1 object per StableValue

Relationship to JEP 500 (Prepare to Make Final Mean Final)

JEP 500 warns against using reflection to mutate final fields. StableValue is the forward-looking solution to the problem JEP 500 is closing: frameworks that need mutable-at-startup but constant-at-runtime fields should migrate to StableValue rather than reflectively mutating final.

// Before: DI framework sets this "final" field via reflection (⚠️ JEP 500 warns)
private final OrderService orderService = null;

// After: use a StableValue — set by the framework, trusted by the JIT
private final StableValue<OrderService> orderService = StableValue.of();

public OrderService getOrderService() {
    return orderService.get();  // JIT-constant after injection
}

Running with --enable-preview

Because StableValue is a preview API, you need --enable-preview at both compile time and runtime:

# Compile
javac --enable-preview --release 26 MyClass.java

# Run
java --enable-preview MyClass

# With Maven (add to compiler and exec plugin configurations)
mvn compile exec:exec \
  -Dexec.executable=java \
  -Dexec.args="--enable-preview -cp target/classes com.example.Main"

Best For

  • Framework bootstrap singletons (application contexts, service registries)
  • Expensive computed constants (compiled regex, crypto keys, parsed configs)
  • Per-key caches where each entry is computed once (route tables, handler maps)
  • Dependency injection containers migrating away from reflective final field mutation
  • Any volatile double-checked locking pattern where the value is logically constant