JEP 456:准备删除 Unsafe 中的内存访问方法

JEP 456:准备删除 Unsafe 中的内存访问方法
2024年06月27日 14:56 InfoQ

作者 | A N M Bazlur Rahman

译者 | 平川

策划 | 丁晓昀

JEP 471(弃用 sun.misc.Unsafe 中的内存访问方法以备删除)已经在 JDK 23 中发布。该 JEP 建议弃用 Unsafe 类中的内存访问方法,以便在将来的版本中删除。这些不再支持的方法已经被标准 API 所取代:JEP 193(变量句柄,已在 JDK 9 中交付)和 JEP 454(外部函数和内存 API,已在 JDK 22 中交付)。

弃用这些方法的主要目的是为最终删除sun.misc.Unsafe中的内存访问方法做准备。编译时和运行时警告会突出显示这些方法的使用情况,开发人员可以借此识别并迁移到受支持的替代方法。这一转变的目标是确保应用程序能够顺利过渡到现代 JDK 版本,从而增强安全性和性能。

现在,有两个标准 API 为sun.misc.Unsafe提供了安全高效的替代方案。VarHandle API(即在 JDK 9 中交付的 JEP 193)提供了安全操作堆内存的方法,可以确保操作有效执行并且不会出现未定义的行为。外部函数和内存 API(即在 JDK 22 中交付的 JEP 454)提供了安全的堆外内存访问方法,通常与 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)

这些方法可以用VarHandleMemorySegment::ofArray及其重载方法代替。例如,考虑下面的例子:

class Foo {

private static final Unsafe UNSAFE = ...; // 一个 sun.misc.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);

}

}

上述代码可以用 VarHandle 实现如下:

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);

}

}

堆外方法主要有以下这些:

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 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 = 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;

}

}

上述代码使用标准 API ArenaMemorySegment 替换后如下:

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 版本。在第一阶段(从 JDK 23 开始),所有内存访问方法都将被弃用,并且将发出编译时警告。第二阶段(计划从 JDK 25 或更早的版本开始)将在发现使用已弃用方法的情况时发出运行时警告。第三阶段(计划从 JDK 26 或更高的版本开始)将进一步升级响应,在发现对这些方法的调用时默认抛出异常。最后,第四和第五阶段将删除已弃用的方法。这两个阶段可能发生在同一版本中。开发人员可以使用新增的命令行选项--sun-misc-unsafe-memory-access={allow|warn|debug|deny}来管理弃用警告并评估对其应用程序的影响。

弃用sun.misc.Unsafe内存访问方法是增强 Java 平台完整性和安全性的一个重要步骤。借助 VarHandle 及外部函数和内存 API,开发人员可以保证其应用程序的健壮性,并兼容未来的 JDK 版本。这种分阶段的方法为迁移提供了充足的时间,既有助于 Java 开发最佳实践的推广,又能够尽可能地减少由此带来的影响。

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部