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

2024-06-27 18:44:46 浏览数 (2)

作者 | 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方法分为三类:堆内、堆外和双模(可以访问堆内和堆外内存的方法)。堆内方法包括:

代码语言:javascript复制
long objectFieldOffset(Field f)
long staticFieldOffset(Field f)
Object staticFieldBase(Field f)
int arrayBaseOffset(Class<?> arrayClass)
int arrayIndexScale(Class<?> arrayClass)

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

代码语言:javascript复制
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 实现如下:

代码语言:javascript复制
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);
    }
}

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

代码语言:javascript复制
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 操作替换。考虑下面的例子:

代码语言:javascript复制
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;
    }

}

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

代码语言:javascript复制
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 开发最佳实践的推广,又能够尽可能地减少由此带来的影响。

原文链接:

https://www.infoq.com/news/2024/06/jep-456-removing-unsafe-methods/

声明:本文由 InfoQ 翻译,未经许可禁止转载。

0 人点赞