JEP 471: 弃用并移除 sun.misc.Unsafe 中的内存访问方法
JEP 471 已为 JDK 23 提交,该提案建议弃用 sun.misc.Unsafe 类中的内存访问方法,并在未来的版本中移除这些方法。这些不推荐使用的方法已被标准 API 取代,分别是 JDK 9 中引入的 JEP 193(变量句柄)和 JDK 22 中引入的 JEP 454(外部函数与内存 API)。
主要目标
该弃用的主要目的是为生态系统的最终移除 sun.misc.Unsafe 的内存访问方法做准备。通过编译时和运行时的警告,开发者可以识别这些方法的使用,并迁移到支持的替代方案。这一过渡旨在确保应用程序能够顺利适应现代 JDK 版本,提升安全性和性能。
替代方案
目前有两个标准 API 提供了安全且高效的替代方案:
- 变量句柄(VarHandle)API:通过 JEP 193 在 JDK 9 中引入,提供了安全操作堆内存的方法,确保操作高效且不会引发未定义行为。
- 外部函数与内存(Foreign Function & Memory)API:通过 JEP 454 在 JDK 22 中引入,提供了安全的堆外内存访问方法,通常与 VarHandle 结合使用,管理 JVM 堆内外的内存。这些 API 承诺无未定义行为、长期稳定性,并更好地与 Java 工具和文档集成。
弃用的方法分类
弃用的 sun.misc.Unsafe 方法分为三类:堆内、堆外和双模式(可以访问堆内和堆外内存)。具体方法如下:
堆内方法
long objectFieldOffset(Field f)
long staticFieldOffset(Field f)
Object staticFieldBase(Field f)
int arrayBaseOffset(Class<?> arrayClass)
int arrayIndexScale(Class<?> arrayClass)这些方法可以被 VarHandle 和 MemorySegment::ofArray 及其重载方法替代。
堆外方法
long allocateMemory(long bytes)
long reallocateMemory(long address, long bytes)
void freeMemory(long address)
void invokeCleaner(java.nio.ByteBuffer directBuffer)
void setMemory(long address, long bytes, byte value)
void copyMemory(long srcAddress, long destAddress, long bytes)
[type] get[Type](long address)
void put[Type](long address, [type] x)这些方法可以被 MemorySegment 操作替代。
迁移示例
堆内方法迁移示例
原始代码:
class Foo {
private static final Unsafe UNSAFE = ...;
private static final long X_OFFSET;
static {
try {
X_OFFSET = UNSAFE.objectFieldOffset(Foo.class.getDeclaredField("x"));
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryToDoubleAtomically() {
int oldValue = x;
return UNSAFE.compareAndSwapInt(this, X_OFFSET, oldValue, oldValue * 2);
}
}迁移后代码:
class Foo {
private static final VarHandle X_VH;
static {
try {
X_VH = MethodHandles.lookup().findVarHandle(Foo.class, "x", int.class);
} catch (Exception ex) { throw new AssertionError(ex); }
}
private int x;
public boolean tryAtomicallyDoubleX() {
int oldValue = x;
return X_VH.compareAndSet(this, oldValue, oldValue * 2);
}
}堆外方法迁移示例
原始代码:
class OffHeapIntBuffer {
private static final Unsafe UNSAFE = ...;
private static final int ARRAY_BASE = UNSAFE.arrayBaseOffset(int[].class);
private static final int ARRAY_SCALE = UNSAFE.arrayIndexScale(int[].class);
private final long size;
private long bufferPtr;
public OffHeapIntBuffer(long size) {
this.size = size;
this.bufferPtr = UNSAFE.allocateMemory(size * ARRAY_SCALE);
}
public void deallocate() {
if (bufferPtr == 0) return;
UNSAFE.freeMemory(bufferPtr);
bufferPtr = 0;
}
private boolean checkBounds(long index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(index);
return true;
}
public void setVolatile(long index, int value) {
checkBounds(index);
UNSAFE.putIntVolatile(null, bufferPtr + ARRAY_SCALE * index, value);
}
public void initialize(long start, long n) {
checkBounds(start);
checkBounds(start + n-1);
UNSAFE.setMemory(bufferPtr + start * ARRAY_SCALE, n * ARRAY_SCALE, 0);
}
public int[] copyToNewArray(long start, int n) {
checkBounds(start);
checkBounds(start + n-1);
int[] a = new int[n];
UNSAFE.copyMemory(null, bufferPtr + start * ARRAY_SCALE, a, ARRAY_BASE, n * ARRAY_SCALE);
return a;
}
}迁移后代码:
class OffHeapIntBuffer {
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
private final Arena arena;
private final MemorySegment buffer;
public OffHeapIntBuffer(long size) {
this.arena = Arena.ofShared();
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
}
public void deallocate() {
arena.close();
}
public void setVolatile(long index, int value) {
ELEM_VH.setVolatile(buffer, 0L, index, value);
}
public void initialize(long start, long n) {
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.fill((byte) 0);
}
public int[] copyToNewArray(long start, int n) {
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.toArray(ValueLayout.JAVA_INT);
}
}迁移阶段
迁移将分几个阶段进行,每个阶段与一个 JDK 版本对齐:
- 阶段 1:从 JDK 23 开始,所有内存访问方法将被弃用,并发出编译时警告。
- 阶段 2:计划在 JDK 25 或更早版本中引入运行时警告。
- 阶段 3:计划在 JDK 26 或更晚版本中,默认情况下在被调用时抛出异常。
- 阶段 4 和 5:移除弃用的方法,可能在同一版本中进行。
命令行选项
开发者可以使用新的命令行选项 --sun-misc-unsafe-memory-access={allow|warn|debug|deny} 来管理弃用警告,并评估对其应用程序的影响。
结论
弃用 sun.misc.Unsafe 的内存访问方法是增强 Java 平台完整性和安全性的重要一步。通过采用 VarHandle 和外部函数与内存 API,开发者可以确保其应用程序在未来 JDK 版本中保持健壮和兼容。分阶段的方法为迁移提供了充足的时间,最大限度地减少了中断,同时促进了 Java 开发中的最佳实践。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。