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: theStableValueAPI 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
finalfield mutation - Any
volatiledouble-checked locking pattern where the value is logically constant