跳过正文
浅尝辄止 JEP - JEP-502:Stable Value(预览)
  1. 文章/

浅尝辄止 JEP - JEP-502:Stable Value(预览)

·4869 字·10 分钟
NeatGuyCoding
作者
NeatGuyCoding

JEP 502 是一个预览版的 JDK 增强提案。对于预览版 JEP,我们浅尝辄止;对于稳定版 JEP,我们深入浅出。这是对 JEP 502:Stable Value(预览)的浅尝辄止。

JEP 链接:https://openjdk.org/jeps/502

1. 概述
#

JEP 502 引入的 StableValue API 旨在解决 Java 开发中的一个根本性矛盾:不可变性与初始化灵活性之间的权衡

1.1. 传统方案的局限性
#

我们经常会遇到这样一个程序设计场景:某个字段在初始化后不应再改变,但其初始化时机可能并不确定,可能需要在运行时动态决定。传统上,Java 提供了两种主要方式来实现这一目标:

final 字段的限制:

  • 必须在构造时或静态初始化时设置,但是实际上我们再编写程序的时候,往往无法在构造函数中就确定某个值
  • 初始化顺序受声明顺序限制
  • 导致应用启动时的"急切初始化"问题

可变字段的问题:

  • 失去常量折叠优化机会
  • 需要复杂的同步机制(需要双重检查锁定 DCL),并且在某些并发访问的场景下字段必须是 volatile 或者通过 VarhandlegetRelease()setAcquire() 方法来保证可见性。
  • DCL 带来的 volatile 字段带来的性能,即使字段被初始化了,访问时仍然需要读取 volatile 字段,volatile 读有额外的内存屏障,但是实际上这个字段其实不再改变了。

1.2. StableValue 的设计
#

StableValue 通过以下设计原则解决了这些问题:

  1. 延迟不可变性(Deferred Immutability):值可以在任何时候设置,但一旦设置就不可变
  2. 最多一次初始化(At-Most-Once Semantics):即使在并发环境下,也只初始化一次
  3. JVM 优化友好:利用 @Stable 注解,让 JVM 可以进行常量折叠优化

1.3. StableValue API 设计
#

核心接口:StableValue<T>

public sealed interface StableValue<T> 
    permits StableValueImpl {
    
    // 设置操作
    boolean trySet(T contents);
    void setOrThrow(T contents);
    
    // 读取操作
    T orElseThrow();
    T orElse(T other);
    T orElseSet(Supplier<? extends T> supplier);
    
    // 状态查询
    boolean isSet();
    
    // 工厂方法
    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);
}

简单看来:这个接口使用 sealed 接口限制实现类,确保:

  • 只有 StableValueImpl 可以实现该接口
  • 防止外部不安全的实现
  • 便于 JVM 进行特殊优化

1.3.1. orElseSet() - 最核心的方法
#

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

设计要点:

  1. 保证最多一次执行:即使在高并发场景下,supplier 也只会执行一次
  2. 线程安全:内部使用同步机制确保线程安全(其实也是 DCL)
  3. 异常处理:如果 supplier 抛出异常,值不会被设置,异常会传播给调用者
  4. 防递归:检测并防止递归初始化(同一个线程重入的时候,即在 orElseSet 里面重复调用 orElseSet,会抛出 IllegalStateException

使用场景对比:

// 传统方式:需要 DCL 手动同步
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 方式:简洁且安全
private final StableValue<Logger> logger = StableValue.of();

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

1.3.2. trySet() vs setOrThrow()
#

两种设置方法的设计差异:

boolean trySet(T contents);  // 返回 false 如果已设置
void setOrThrow(T contents);   // 抛出异常如果已设置

设计考虑:

  • trySet():适合"尝试设置,失败也无妨"的场景
  • setOrThrow():适合"必须设置成功"的场景,提供明确的错误信息

1.3.3. 高级抽象:Stable Supplier、Function、Collection
#

1.3.3.1. Stable Supplier
#

static <T> Supplier<T> supplier(Supplier<? extends T> underlying);
  • 将初始化和访问逻辑封装在声明处
  • 客户端代码更简洁:logger.get() vs getLogger()
  • 减少样板代码(不需要单独的 getter 方法)

实现方式:

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

底层还是使用 orElseSet() 实现的。

1.3.3.2. Stable List
#

static <T> List<T> list(int size, IntFunction<? extends T> function);
  • 固定大小的不可变列表
  • 每个元素独立延迟初始化
  • 支持随机访问(实现 RandomAccess 接口)
  • 适合对象池模式

使用示例:

// 创建对象池
static final List<OrderController> POOL = 
    StableValue.list(POOL_SIZE, index -> new OrderController());

// 按线程 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):只对预定义的键集合有效
  • 适合缓存计算结果(如查找表、转换表)
  • 支持常量折叠优化

使用示例:

// 计算对数查找表
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);  // 如果入参属于 KEYS,会被 JIT 折叠为常量
}

2. 核心实现源码简要分析
#

StableValue 的核心实现类是 StableValueImpl

2.1. StableValueImpl 的核心字段
#

文件位置src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

// Unsafe 允许在启动序列早期使用 StableValue
static final Unsafe UNSAFE = Unsafe.getUnsafe();

// 用于直接字段访问的偏移量
private static final long CONTENTS_OFFSET =
        UNSAFE.objectFieldOffset(StableValueImpl.class, "contents");

// 用于表示 null 值的哨兵对象
private static final Object NULL_SENTINEL = new Object();

// 使用 @Stable 注解标记的核心字段
// | Value          |  Meaning      |
// | -------------- |  ------------ |
// | null           |  Unset        |
// | NULL_SENTINEL  |  Set(null)    |
// | other          |  Set(other)   |
@Stable
private Object contents;

使用 Unsafe 的原因

  • 允许在 JVM 启动早期使用(不依赖反射、MethodHandles)
  • 可以精确控制内存语义(acquire/release)
  • 避免方法调用的开销

NULL_SENTINEL 设计

  • Java 中 null 无法区分"未设置"和"设置为 null"
  • 使用哨兵对象解决这个问题
  • 增加少量内存开销,但保证语义正确性

JDK 内部使用的 @Stable 注解的作用

  • 告诉 JVM 该字段在初始化后值稳定
  • 允许 JIT 进行常量折叠优化

2.2. 核心方法实现分析
#

2.2.1. 读取操作:wrappedContentsAcquire()
#

文件位置src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

@ForceInline
public Object wrappedContentsAcquire() {
    return UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET);
}
  • Acquire 语义:确保在读取 contents 之前,所有之前的内存操作都已完成。Release/Acquire 语义一般成对出现,内存屏障比 volatile 更轻量:
    • Release/Acquire:JMM 抽象上,Release 之前放 LoadStoreStoreStore 屏障;Acquire 之后放 LoadLoadLoadStroe 屏障
    • volatile:JMM 抽象上,volatile write 之前放 LoadStoreStoreStore 屏障,之后放StroeLoadvolatile read 之后放 LoadLoadLoadStroe 屏障。
    • StoreLoad 屏障开销一般比较大,一般 CPU 与操作系统中都是全屏障。
  • 使用 @ForceInline 提示 JVM 内联此方法,减少调用开销

2.2.2. 设置操作:trySet() 和 wrapAndSet()
#

文件位置src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

@ForceInline
@Override
public boolean trySet(T contents) {
    // 快速路径:如果已设置,直接返回 false
    if (wrappedContentsAcquire() != null) {
        return false;
    }
    // 防止递归初始化
    preventReentry();
    // 互斥锁保护,与 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);
    // 在持有锁的情况下,普通语义足够
    if (contents == null) {
        // 使用 Release 语义写入,确保之前的所有操作可见
        UNSAFE.putReferenceRelease(this, CONTENTS_OFFSET, wrap(newValue));
        return true;
    }
    return false;
}

双重检查锁定模式(DCL)

  • 第一次检查:wrappedContentsAcquire() != null(快速路径)
  • 加锁后再次检查:contents == null(慢速路径)
  • 避免不必要的锁竞争

内存屏障顺序

  • 读取:使用 getReferenceAcquire(acquire 语义)
  • 写入:使用 putReferenceRelease(release 语义)
  • 形成 happens-before 关系

防递归机制:防止同一个线程递归调用导致重复初始化。

2.2.3. 延迟初始化:orElseSet()
#

文件位置src/java.base/share/classes/jdk/internal/lang/stable/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);
}

@DontInline  // 明确标记不要内联,保持方法边界清晰
private T orElseSetSlowPath(Supplier<? extends T> supplier) {
    preventReentry();  // 防止递归
    synchronized (this) {
        // 在锁内再次检查(双重检查)
        final Object t = contents;  // 普通语义足够(已持有锁)
        if (t == null) {
            // 执行 supplier,可能耗时较长
            final T newValue = supplier.get();
            // 设置值并返回
            wrapAndSet(newValue);
            return newValue;
        }
        // 其他线程已设置,直接返回
        return unwrap(t);
    }
}

快速路径优化

  • 大部分情况下值已设置,快速返回
  • 使用 @ForceInline 内联主方法
  • 避免不必要的同步开销

慢速路径分离

  • 使用 @DontInline 标记慢速路径
  • 保持方法边界,便于调试和性能分析
  • 避免过度内联导致代码膨胀

2.2.4. null 值处理机制
#

文件位置src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java

// 将 null 值包装为哨兵对象
@ForceInline
private static Object wrap(Object t) {
    return (t == null) ? NULL_SENTINEL : t;
}

// 将哨兵对象解包为 null
@SuppressWarnings("unchecked")
@ForceInline
private static <T> T unwrap(Object t) {
    return t != NULL_SENTINEL ? (T) t : null;
}

为什么需要这个机制?

在 Java 中,对象引用字段无法区分以下两种情况:

  • 字段未初始化(null)
  • 字段已初始化为 null 值

StableValue 需要区分这两种状态,因此:

  • 使用 null 表示"未设置"
  • 使用 NULL_SENTINEL 表示"已设置为 null"

内存开销:

  • 每个 StableValue 实例:一个对象引用(8 字节)
  • NULL_SENTINEL:全局共享对象(每个 JVM 一个实例)
  • 总开销:几乎可以忽略

2.2.5. 高级抽象实现
#

2.2.5.1. StableSupplier 实现
#

文件位置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);
    }
    
    // 基于身份的 equals 和 hashCode
    @Override
    public int hashCode() {
        return System.identityHashCode(this);
    }
    
    @Override
    public boolean equals(Object obj) {
        return obj == this;
    }
}

底层仍然使用 StableValueImplorElseSet() 方法实现延迟初始化。

2.2.5.2. StableFunction 实现
#

文件位置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) {
        // 查找对应的 StableValue
        final StableValueImpl<R> stable = values.get(value);
        if (stable == null) {
            throw new IllegalArgumentException("Input not allowed: " + value);
        }
        // 延迟初始化并返回
        return stable.orElseSet(new Supplier<R>() {
            @Override public R get() { 
                return original.apply(value); 
            }
        });
    }
}

Map 结构

  • 每个输入值对应一个 StableValueImpl,底层还是使用 orElseSet() 实现延迟初始化
  • 独立的延迟初始化
  • 支持部分函数(只允许预定义的输入)

异常处理

  • 不允许的输入立即抛出异常
  • 不创建不必要的 StableValue 实例
  • 提供清晰的错误信息

3. JIT 优化
#

JDK 层面通过 StableValue 使用了 @Stable 注解,JIT 层面通过识别 StableValue 的使用模式,进行优化。

3.1. @Stable 注解的作用
#

@Stable 字段优化支持:

  • C1 编译器:支持基本的常量折叠,但优化程度有限
  • C2 编译器:支持完整的常量折叠和更激进的优化(死代码消除、循环展开等)
flowchart TD
    A[Java 字节码: 读取字段] --> B{编译器接口 CI
ciField.cpp
C1 和 C2 共享} B --> C[检查字段属性
is_stable?] C --> D{字段是
Stable注解的?} D -->|是| E[标记为常量
_is_constant = true
C1 和 C2 共享] D -->|否| F[普通字段处理] E --> G{编译器选择} G -->|C1 路径| H[C1 GraphBuilder
c1_GraphBuilder.cpp] G -->|C2 路径| I[C2 Parser
parse3.cpp] H --> J[创建 Constant 节点
make_constant
支持 StableArrayConstant] I --> K[创建常量节点
make_constant_from_field
跳过内存读取] J --> L[C1 优化阶段
基本常量折叠] K --> M[C2 优化阶段
激进优化] M --> N[消除内存别名分析
死代码消除
循环展开
内联优化] L --> O[生成机器码
值直接嵌入] N --> O O --> P[运行时执行
零内存访问] style B fill:#e1f5ff style E fill:#fff4e1 style H fill:#ffe1e1 style I fill:#e1ffe1 style M fill:#e1ffe1 style N fill:#e1ffe1

3.2. JIT 优化后的 StableValue 核心方法 orElseSet() 的效果
#

原始代码:

// 调用方代码
Logger logger = stableValue.orElseSet(() -> Logger.create(...));

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

如果代码进入 JIT 优化,一定是被调用了很多次,那么这个值一定被初始化了。首先是 C1 优化,第一步,方法内联:

由于 @ForceInline 注解,C1 会内联所有 @ForceInline 的方法:

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

其中:

  • wrappedContentsAcquire()@ForceInline,内联为:UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET)
  • unwrap(t)@ForceInline,内联为:(t != NULL_SENTINEL ? (T) t : null)

接下来,C1 会进行常量折叠优化:

Logger logger;
{
    Objects.requireNonNull(supplier);
    final Object t = <常量值>;  // 直接使用常量,无需内存读取
    logger = (t == null) ? orElseSetSlowPath(supplier) : (t != NULL_SENTINEL ? (T) t : null);
}

如果代码运行足够多次,进入 C2,C2 会做前面的优化,然后进行更激进的优化,首先是死代码消除(Dead Code Elimination):

// C2 死代码消除后
Logger logger;
{
    Objects.requireNonNull(supplier);  // 可能被消除(如果编译器能证明 supplier 非 null)
    final Object t = <常量值>;  // 已知 != null
    // 分支 (t == null) 被证明为 false
    // orElseSetSlowPath(supplier) 调用被完全消除(死代码)
    logger = t != NULL_SENTINEL ? (T) t : null;  // 只保留这个路径
}

如果 C2 能证明 t != NULL_SENTINEL(大多数情况),可以进一步优化:

// C2 最终优化结果(t != NULL_SENTINEL)
Logger logger = <常量值>;  // 直接赋值,零开销

或者如果 t == NULL_SENTINEL

// C2 最终优化结果(t == NULL_SENTINEL)
Logger logger = null;  // 直接返回 null

相关文章

全网最硬核 JDK 分析 - 3. Java 新内存模型解析与实验

·25486 字·51 分钟
从规范到实现深入探讨 Java 内存模型(JMM),涵盖内存屏障、CPU 重排序和 Java 9+ VarHandle API。了解一致性、因果性、共识性,以及 volatile、final 和其他同步机制在底层的工作原理,并提供实用的 jcstress 示例。

为什么应该避免在生产环境中启用 HeapDumpOnOutOfMemoryError

·1536 字·4 分钟
全面指南,探讨为什么启用 HeapDumpOnOutOfMemoryError 会在生产环境中导致严重的性能问题,哪些 OutOfMemoryError 类型实际触发堆转储,以及使用 JFR 进行内存泄漏检测和自动服务重启策略等更好的替代方案。