Skip to main content
A Brief Look at JEP - JEP-502: Stable Value (Preview)
  1. Posts/

A Brief Look at JEP - JEP-502: Stable Value (Preview)

·2181 words·5 mins
NeatGuyCoding
Author
NeatGuyCoding
Table of Contents

JEP 502 is a preview JDK Enhancement Proposal. For preview JEPs, we take a brief look; for stable JEPs, we dive deep. This is a brief look at JEP 502: Stable Value (Preview).

JEP Link: https://openjdk.org/jeps/502

1. Overview
#

The StableValue API introduced by JEP 502 aims to solve a fundamental contradiction in Java development: the trade-off between immutability and initialization flexibility.

1.1. Limitations of Traditional Approaches
#

We often encounter this design scenario: a field should not change after initialization, but its initialization timing may be uncertain and may need to be determined dynamically at runtime. Traditionally, Java provides two main approaches to achieve this:

Limitations of final fields:

  • Must be set during construction or static initialization, but in practice, we often cannot determine a value in the constructor when writing programs
  • Initialization order is constrained by declaration order
  • Leads to “eager initialization” problems at application startup

Problems with mutable fields:

  • Lose constant folding optimization opportunities
  • Require complex synchronization mechanisms (requiring double-checked locking DCL), and in certain concurrent access scenarios, fields must be volatile or use VarHandle’s getRelease() and setAcquire() methods to ensure visibility
  • Performance impact of volatile fields from DCL: even after the field is initialized, accessing it still requires reading the volatile field. Volatile reads have additional memory barriers, but the field actually no longer changes

1.2. StableValue Design
#

StableValue solves these problems through the following design principles:

  1. Deferred Immutability: Values can be set at any time, but once set, they are immutable
  2. At-Most-Once Semantics: Even in concurrent environments, initialization happens only once
  3. JVM Optimization Friendly: Uses the @Stable annotation to enable constant folding optimizations by the JVM

1.3. StableValue API Design
#

Core interface: StableValue<T>

public sealed interface StableValue<T> 
    permits StableValueImpl {
    
    // Setting operations
    boolean trySet(T contents);
    void setOrThrow(T contents);
    
    // Reading operations
    T orElseThrow();
    T orElse(T other);
    T orElseSet(Supplier<? extends T> supplier);
    
    // Status query
    boolean isSet();
    
    // Factory methods
    static <T> StableValue<T> of();
    static <T> StableValue<T> of(T contents);
    static <T> Supplier<T> supplier(Supplier<? extends T> underlying);
    static <T> List<T> list(int size, IntFunction<? extends T> function);
    static <K, V> Map<K, V> map(Set<K> keys, Function<? super K, ? extends V> function);
    static <T> IntFunction<T> intFunction(int size, IntFunction<? extends T> function);
    static <T> Function<T, R> function(Set<T> keys, Function<? super T, ? extends R> function);
}

Simply put: This interface uses a sealed interface to restrict implementations, ensuring:

  • Only StableValueImpl can implement this interface
  • Prevents unsafe external implementations
  • Facilitates special optimizations by the JVM

1.3.1. orElseSet() - The Core Method
#

T orElseSet(Supplier<? extends T> supplier);

Design Points:

  1. Guaranteed at-most-once execution: Even in high-concurrency scenarios, the supplier executes only once
  2. Thread-safe: Internal synchronization ensures thread safety (also using DCL)
  3. Exception handling: If the supplier throws an exception, the value is not set, and the exception propagates to the caller
  4. Recursion prevention: Detects and prevents recursive initialization (when the same thread re-enters, i.e., calling orElseSet again within orElseSet, it throws IllegalStateException)

Usage Comparison:

// Traditional approach: requires manual DCL synchronization
private Logger logger = null;
private final Object lock = new Object();

Logger getLogger() {
    if (logger == null) {
        synchronized (lock) {
            if (logger == null) {
                logger = Logger.create(...);
            }
        }
    }
    return logger;
}

// StableValue approach: concise and safe
private final StableValue<Logger> logger = StableValue.of();

Logger getLogger() {
    return logger.orElseSet(() -> Logger.create(...));
}

1.3.2. trySet() vs setOrThrow()
#

Design differences between the two setting methods:

boolean trySet(T contents);  // Returns false if already set
void setOrThrow(T contents);   // Throws exception if already set

Design Considerations:

  • trySet(): Suitable for “try to set, failure is acceptable” scenarios
  • setOrThrow(): Suitable for “must set successfully” scenarios, providing clear error messages

1.3.3. Advanced Abstractions: Stable Supplier, Function, Collection
#

1.3.3.1. Stable Supplier
#

static <T> Supplier<T> supplier(Supplier<? extends T> underlying);
  • Encapsulates initialization and access logic at the declaration point
  • More concise client code: logger.get() vs getLogger()
  • Reduces boilerplate code (no need for separate getter methods)

Implementation:

record StableSupplier<T>(StableValueImpl<T> delegate,
                         Supplier<? extends T> original) 
    implements Supplier<T> {
    
    @ForceInline
    @Override
    public T get() {
        return delegate.orElseSet(original);
    }
}

The underlying implementation still uses orElseSet() from StableValueImpl.

1.3.3.2. Stable List
#

static <T> List<T> list(int size, IntFunction<? extends T> function);
  • Fixed-size immutable list
  • Each element independently lazy-initialized
  • Supports random access (implements RandomAccess interface)
  • Suitable for object pool patterns

Usage Example:

// Create object pool
static final List<OrderController> POOL = 
    StableValue.list(POOL_SIZE, index -> new OrderController());

// Allocate by thread ID
OrderController getController() {
    int index = (int)(Thread.currentThread().getId() % POOL_SIZE);
    return POOL.get(index);
}

1.3.3.3. Stable Function
#

static <T, R> Function<T, R> function(Set<T> keys, Function<? super T, ? extends R> function);
  • Partial function: only valid for predefined key sets
  • Suitable for caching computed results (e.g., lookup tables, conversion tables)
  • Supports constant folding optimization

Usage Example:

// Compute logarithm lookup table
private static final Set<Integer> KEYS = Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2 = 
    StableValue.function(KEYS, i -> 31 - Integer.numberOfLeadingZeros(i));

public static int log2(int n) {
    return LOG2.apply(n);  // If input belongs to KEYS, it will be folded to a constant by JIT
}

2. Brief Analysis of Core Implementation Source Code
#

The core implementation class of StableValue is StableValueImpl

2.1. Core Fields of StableValueImpl
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

// Unsafe allows using StableValue early in the startup sequence
static final Unsafe UNSAFE = Unsafe.getUnsafe();

// Offset for direct field access
private static final long CONTENTS_OFFSET =
        UNSAFE.objectFieldOffset(StableValueImpl.class, "contents");

// Sentinel object to represent null values
private static final Object NULL_SENTINEL = new Object();

// Core field marked with @Stable annotation
// | Value          |  Meaning      |
// | -------------- |  ------------ |
// | null           |  Unset        |
// | NULL_SENTINEL  |  Set(null)    |
// | other          |  Set(other)   |
@Stable
private Object contents;

Reasons for using Unsafe: - Allows use early in JVM startup (doesn’t depend on reflection, MethodHandles) - Can precisely control memory semantics (acquire/release) - Avoids method call overhead

NULL_SENTINEL Design: - In Java, null cannot distinguish between “unset” and “set to null” - Uses a sentinel object to solve this problem - Adds slight memory overhead but ensures semantic correctness

Role of the @Stable annotation used internally in JDK: - Tells the JVM that the field’s value is stable after initialization - Allows JIT to perform constant folding optimizations

2.2. Core Method Implementation Analysis
#

2.2.1. Read Operation: wrappedContentsAcquire()
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

@ForceInline
public Object wrappedContentsAcquire() {
    return UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET);
}
  • Acquire semantics: Ensures that before reading contents, all previous memory operations are completed. Release/Acquire semantics generally appear in pairs, and memory barriers are lighter than volatile:
    • Release/Acquire: In JMM abstraction, Release places LoadStore and StoreStore barriers before; Acquire places LoadLoad and LoadStore barriers after
    • volatile: In JMM abstraction, volatile write places LoadStore and StoreStore barriers before, and StoreLoad after; volatile read places LoadLoad and LoadStore barriers after
    • StoreLoad barriers generally have significant overhead, typically full barriers in CPUs and operating systems
  • Uses @ForceInline to hint the JVM to inline this method, reducing call overhead

2.2.2. Setting Operations: trySet() and wrapAndSet()
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

@ForceInline
@Override
public boolean trySet(T contents) {
    // Fast path: if already set, return false directly
    if (wrappedContentsAcquire() != null) {
        return false;
    }
    // Prevent recursive initialization
    preventReentry();
    // Protected by mutex lock, coordinated with orElseSet
    synchronized (this) {
        return wrapAndSet(contents);
    }
}

private void preventReentry() {
   if (Thread.holdsLock(this)) {
       throw new IllegalStateException("Recursive initialization...");
   }
}

@ForceInline
private boolean wrapAndSet(T newValue) {
    assert Thread.holdsLock(this);
    // Under lock, plain semantics are sufficient
    if (contents == null) {
        // Use Release semantics to write, ensuring all previous operations are visible
        UNSAFE.putReferenceRelease(this, CONTENTS_OFFSET, wrap(newValue));
        return true;
    }
    return false;
}

Double-Checked Locking Pattern (DCL):

  • First check: wrappedContentsAcquire() != null (fast path)
  • Second check after locking: contents == null (slow path)
  • Avoids unnecessary lock contention

Memory barrier ordering:

  • Read: uses getReferenceAcquire (acquire semantics)
  • Write: uses putReferenceRelease (release semantics)
  • Forms happens-before relationship

Recursion prevention mechanism: Prevents recursive calls by the same thread causing repeated initialization.

2.2.3. Lazy Initialization: orElseSet()
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

@ForceInline
@Override
public T orElseSet(Supplier<? extends T> supplier) {
    Objects.requireNonNull(supplier);
    // Fast path: if already set, return directly
    final Object t = wrappedContentsAcquire();
    return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t);
}

@DontInline  // Explicitly marked not to inline, keeping method boundaries clear
private T orElseSetSlowPath(Supplier<? extends T> supplier) {
    preventReentry();  // Prevent recursion
    synchronized (this) {
        // Check again inside lock (double-check)
        final Object t = contents;  // Plain semantics sufficient (lock already held)
        if (t == null) {
            // Execute supplier, may take time
            final T newValue = supplier.get();
            // Set value and return
            wrapAndSet(newValue);
            return newValue;
        }
        // Other thread has set it, return directly
        return unwrap(t);
    }
}

Fast path optimization:

  • In most cases, the value is already set, return quickly
  • Uses @ForceInline to inline the main method
  • Avoids unnecessary synchronization overhead

Slow path separation:

  • Uses @DontInline to mark the slow path
  • Maintains method boundaries for debugging and performance analysis
  • Avoids code bloat from excessive inlining

2.2.4. Null Value Handling Mechanism
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

// Wrap null value as sentinel object
@ForceInline
private static Object wrap(Object t) {
    return (t == null) ? NULL_SENTINEL : t;
}

// Unwrap sentinel object to null
@SuppressWarnings("unchecked")
@ForceInline
private static <T> T unwrap(Object t) {
    return t != NULL_SENTINEL ? (T) t : null;
}

Why is this mechanism needed?

In Java, object reference fields cannot distinguish between the following two cases:

  • Field uninitialized (null)
  • Field initialized to null value

StableValue needs to distinguish these two states, therefore:

  • Uses null to represent “unset”
  • Uses NULL_SENTINEL to represent “set to null”

Memory overhead:

  • Each StableValue instance: one object reference (8 bytes)
  • NULL_SENTINEL: globally shared object (one instance per JVM)
  • Total overhead: almost negligible

2.2.5. Advanced Abstraction Implementations
#

2.2.5.1. StableSupplier Implementation
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java

public record StableSupplier<T>(StableValueImpl<T> delegate,
                                Supplier<? extends T> original) 
    implements Supplier<T> {
    
    @ForceInline
    @Override
    public T get() {
        return delegate.orElseSet(original);
    }
    
    // Identity-based equals and hashCode
    @Override
    public int hashCode() {
        return System.identityHashCode(this);
    }
    
    @Override
    public boolean equals(Object obj) {
        return obj == this;
    }
}

The underlying implementation still uses the orElseSet() method from StableValueImpl for lazy initialization.

2.2.5.2. StableFunction Implementation
#

File Location: src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java

public record StableFunction<T, R>(
    Map<? extends T, StableValueImpl<R>> values,
    Function<? super T, ? extends R> original) 
    implements Function<T, R> {
    
    @ForceInline
    @Override
    public R apply(T value) {
        // Find corresponding StableValue
        final StableValueImpl<R> stable = values.get(value);
        if (stable == null) {
            throw new IllegalArgumentException("Input not allowed: " + value);
        }
        // Lazy initialize and return
        return stable.orElseSet(new Supplier<R>() {
            @Override public R get() { 
                return original.apply(value); 
            }
        });
    }
}

Map structure:

  • Each input value corresponds to a StableValueImpl, still using orElseSet() for lazy initialization
  • Independent lazy initialization
  • Supports partial functions (only allows predefined inputs)

Exception handling:

  • Disallowed inputs throw exceptions immediately
  • Doesn’t create unnecessary StableValue instances
  • Provides clear error messages

3. JIT Optimization
#

At the JDK level, StableValue uses the @Stable annotation; at the JIT level, it recognizes StableValue usage patterns and performs optimizations.

3.1. Role of the @Stable Annotation
#

@Stable field optimization support:

  • C1 Compiler: Supports basic constant folding, but optimization is limited
  • C2 Compiler: Supports full constant folding and more aggressive optimizations (dead code elimination, loop unrolling, etc.)

Mermaid图表

3.2. Effect of JIT Optimization on StableValue Core Method orElseSet()
#

Original code:

// Calling code
Logger logger = stableValue.orElseSet(() -> Logger.create(...));

// orElseSet method in StableValueImpl.java
@ForceInline
@Override
public T orElseSet(Supplier<? extends T> supplier) {
    Objects.requireNonNull(supplier);
    final Object t = wrappedContentsAcquire();
    return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t);
}

If the code enters JIT optimization, it must have been called many times, so the value must be initialized. First is C1 optimization, step one, method inlining:

Due to the @ForceInline annotation, C1 will inline all methods marked with @ForceInline:

Logger logger;
{
    Objects.requireNonNull(supplier);  // supplier is a lambda
    final Object t = UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET);
    logger = (t == null) ? orElseSetSlowPath(supplier) : (t != NULL_SENTINEL ? (T) t : null);
}

Where:

  • wrappedContentsAcquire() is @ForceInline, inlined as: UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET)
  • unwrap(t) is @ForceInline, inlined as: (t != NULL_SENTINEL ? (T) t : null)

Next, C1 will perform constant folding optimization:

Logger logger;
{
    Objects.requireNonNull(supplier);
    final Object t = <constant value>;  // Directly use constant, no memory read needed
    logger = (t == null) ? orElseSetSlowPath(supplier) : (t != NULL_SENTINEL ? (T) t : null);
}

If the code runs enough times and enters C2, C2 will do the previous optimizations, then perform more aggressive optimizations, first dead code elimination:

// After C2 dead code elimination
Logger logger;
{
    Objects.requireNonNull(supplier);  // May be eliminated (if compiler can prove supplier is non-null)
    final Object t = <constant value>;  // Known != null
    // Branch (t == null) proven to be false
    // orElseSetSlowPath(supplier) call completely eliminated (dead code)
    logger = t != NULL_SENTINEL ? (T) t : null;  // Only this path remains
}

If C2 can prove t != NULL_SENTINEL (most cases), it can further optimize:

// C2 final optimization result (t != NULL_SENTINEL)
Logger logger = <constant value>;  // Direct assignment, zero overhead

Or if t == NULL_SENTINEL:

// C2 final optimization result (t == NULL_SENTINEL)
Logger logger = null;  // Directly return null

Related

Why HeapDumpOnOutOfMemoryError Should Be Avoided in Production

·702 words·4 mins
A comprehensive guide exploring why enabling HeapDumpOnOutOfMemoryError can cause significant performance issues in production environments, which OutOfMemoryError types actually trigger heap dumps, and better alternatives like JFR for memory leak detection and automatic service restart strategies.