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 或者通过
Varhandle的getRelease()和setAcquire()方法来保证可见性。 - DCL 带来的 volatile 字段带来的性能,即使字段被初始化了,访问时仍然需要读取 volatile 字段,volatile 读有额外的内存屏障,但是实际上这个字段其实不再改变了。
1.2. StableValue 的设计#
StableValue 通过以下设计原则解决了这些问题:
- 延迟不可变性(Deferred Immutability):值可以在任何时候设置,但一旦设置就不可变
- 最多一次初始化(At-Most-Once Semantics):即使在并发环境下,也只初始化一次
- 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);
设计要点:
- 保证最多一次执行:即使在高并发场景下,supplier 也只会执行一次
- 线程安全:内部使用同步机制确保线程安全(其实也是 DCL)
- 异常处理:如果 supplier 抛出异常,值不会被设置,异常会传播给调用者
- 防递归:检测并防止递归初始化(同一个线程重入的时候,即在
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()vsgetLogger() - 减少样板代码(不需要单独的 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之前放LoadStore和StoreStore屏障;Acquire之后放LoadLoad和LoadStroe屏障volatile:JMM 抽象上,volatile write之前放LoadStore和StoreStore屏障,之后放StroeLoad;volatile read之后放LoadLoad和LoadStroe屏障。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;
}
}
底层仍然使用 StableValueImpl 的 orElseSet() 方法实现延迟初始化。
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



