跳到主要内容

JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法以进行移除

QWen Max 中英对照 JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal

总结

弃用 sun.misc.Unsafe 中的内存访问方法,并计划在未来的版本中移除这些方法。这些不受支持的方法已经被标准 API 所取代,即 VarHandle API(JEP 193,JDK 9)和 Foreign Function & Memory API(JEP 454,JDK 22)。我们强烈鼓励库开发者从 sun.misc.Unsafe 迁移到受支持的替代方案,以便应用程序能够顺利迁移到现代的 JDK 版本。

目标

  • 为在未来 JDK 版本中移除 sun.misc.Unsafe 中的内存访问方法做好生态系统准备。

  • 帮助开发者意识到他们的应用程序何时直接或间接依赖于 sun.misc.Unsafe 中的内存访问方法。

非目标

  • 完全移除 sun.misc.Unsafe 类并不是目标。它的一小部分方法并不用于内存访问;这些方法将会被单独弃用和移除。

  • 并不打算更改 jdk.unsupported 模块中的其他 sun.* 类。

动机

sun.misc.Unsafe 类于 2002 年被引入,作为 JDK 中的 Java 类执行低级操作的一种方式。它的大多数方法(87 个中的 79 个)用于访问内存,要么是在 JVM 的垃圾回收堆中,要么是在堆外内存(不由 JVM 控制)中。正如类名所示,这些内存访问方法是不安全的:它们可能导致未定义行为,包括 JVM 崩溃。因此,它们并未被暴露为标准 API。它们既不是为广泛的客户端使用而设计的,也不是为了永久存在。相反,它们的引入是基于这样的假设:它们专供 JDK 内部使用,并且 JDK 内的调用者在使用它们之前会执行详尽的安全检查,最终会为该功能添加安全的标准 API 到 Java 平台中。

然而,在 2002 年,没有任何方法可以阻止 sun.misc.Unsafe 在 JDK 外部使用,因此它的内存访问方法成为了库开发者们想要比标准 API 提供更多功能和性能的便捷工具。例如,sun.misc.Unsafe::compareAndSwap 可以在字段上执行 CAS(比较并交换)操作,而无需承担 java.util.concurrent.atomic API 的开销,而 sun.misc.Unsafe::setMemory 则可以操作堆外内存,且没有 java.nio.ByteBuffer 的 2GB 限制。依赖 ByteBuffer 来操作堆外内存的库(如 Apache Hadoop 和 Cassandra)通过使用 sun.misc.Unsafe::invokeCleaner 来提高效率,及时释放堆外内存。

不幸的是,并非所有库在调用内存访问方法之前都会勤于执行安全检查,因此应用程序存在失败和崩溃的风险。某些对这些方法的使用是不必要的,驱动力来自于从在线论坛复制粘贴的便利性。其他对这些方法的使用可能会导致 JVM 禁用优化,结果比使用普通的 Java 数组性能更差。然而,由于内存访问方法的使用非常普遍,sun.misc.Unsafe 在 JDK 9 中并未与其他低级 API 一起被封装(JEP 260)。在安全且受支持的替代方案可用之前,它在 JDK 22 中仍然开箱即用。

在过去的几年中,我们引入了两个标准 API,它们是 sun.misc.Unsafe 中内存访问方法的安全且高效的替代品:

这些标准 API 保证没有未定义行为,承诺长期稳定性,并且与 Java 平台的工具和文档有高质量的集成(其使用的示例见下方)。鉴于这些 API 的可用性,现在适当地弃用并最终移除 sun.misc.Unsafe 中的内存访问方法。

移除 sun.misc.Unsafe 中的内存访问方法是确保 Java 平台默认具备完整性 的长期协调工作的一部分。其他举措包括对 Java 本地接口(JNI,JEP 472)和动态加载代理(JEP 451)施加限制。这些努力将使 Java 平台更加安全且性能更高,同时降低应用开发者因使用不支持的 API 而在升级到较新的 JDK 版本时被锁定在旧版本的风险。

描述

sun.misc.Unsafe 的内存访问方法可以分为三类:

  • 访问堆内存的方法(on-heap),
  • 访问堆外内存的方法(off-heap),以及
  • 访问堆内存和堆外内存的方法(bimodal — 一种双模式方法,其参数要么引用堆上的对象,要么为 null 以表示堆外访问)。

我们将分阶段弃用并移除这些方法,每个阶段都在一个单独的 JDK 特性发布中进行:

  1. 弃用所有内存访问方法 —— 堆内、堆外和双模式 —— 以进行移除。这将导致引用这些方法的代码在编译时出现弃用警告,提醒库开发者它们即将被移除。下面描述的一个新的命令行选项将使应用程序开发者和用户在使用这些方法时收到运行时警告。

    与弃用警告不同,javac 自 2006 年以来一直在对使用 sun.misc.Unsafe 发出警告:

    警告:Unsafe 是内部专有 API,将来可能会删除

    这些警告将继续发出,并且无法被抑制。

  2. 当使用内存访问方法时(无论是直接使用还是通过反射使用),在运行时发出警告,详见下文。这将提醒应用程序开发者和用户这些方法即将被移除,并需要升级库。

  3. 当使用内存访问方法时(无论是直接使用还是通过反射使用),抛出异常。这将进一步提醒应用程序开发者和用户这些方法即将被移除。

  4. 移除 堆内 方法。这些方法将首先被移除,因为自 2017 年 JDK 9 以来已经有标准的替代方案。

  5. 移除 堆外双模式 方法。这些方法将稍后移除,因为自 2023 年 JDK 22 以来才有标准的替代方案。

至于时间安排,我们计划实施

  • 第一阶段,通过此 JEP,在 JDK 23 中;
  • 第二阶段,发出运行时警告,在 JDK 25 或之前;
  • 第三阶段,默认抛出异常,在 JDK 26 或之后;以及
  • 第四和第五阶段,将在 JDK 26 之后的版本中移除这些方法。

如果时机成熟,且条件允许,我们可以同时实施第 4 阶段和第 5 阶段。

允许使用 sun.misc.Unsafe 中的内存访问方法

绝大多数 Java 开发者不会在自己的代码中显式使用 sun.misc.Unsafe。然而,许多应用程序直接或间接依赖于使用 sun.misc.Unsafe 的内存访问方法的库。从 JDK 23 开始,应用开发者可以通过使用一个新的命令行选项 --sun-misc-unsafe-memory-access={allow|warn|debug|deny} 来评估这些方法的弃用和移除对库的影响。该选项在精神和形式上类似于 JDK 9 中 JEP 261 引入的 --illegal-access 选项。其工作原理如下:

  • --sun-misc-unsafe-memory-access=allow 允许在运行时使用内存访问方法且不发出警告。

  • --sun-misc-unsafe-memory-access=warn 允许使用内存访问方法,但在首次使用任何内存访问方法时(无论是直接调用还是通过反射)会发出警告。也就是说,无论使用了哪些内存访问方法或某个特定方法被调用了多少次,最多只会发出一次警告。

  • --sun-misc-unsafe-memory-access=debug 允许使用内存访问方法,但在每次使用任何内存访问方法时(无论是直接调用还是通过反射)都会发出一行警告和堆栈跟踪信息。

  • --sun-misc-unsafe-memory-access=deny 通过在每次使用此类方法时(无论是直接调用还是通过反射)抛出 UnsupportedOperationException 来禁止使用内存访问方法。

选项值为 warn 时启用的警告示例如下:

WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::setMemory has been called by com.foo.bar.Server (file:/tmp/foobarserver/thing.jar)
WARNING: Please consider reporting this to the maintainers of com.foo.bar.Server
WARNING: sun.misc.Unsafe::setMemory will be removed in a future release

--sun-misc-unsafe-memory-access 选项的默认值将在我们逐步经历上述阶段时随着每个版本的发布而改变:

  • allow 是阶段 1 中的默认值,就好像每次调用 java 启动器都包含 --sun-misc-unsafe-memory-access=allow 一样。

  • warn 将是阶段 2 中的默认值。在阶段 2 中,可以将值从 warn 改回 allow,从而避免警告。

  • deny 将是阶段 3 中的默认值。在阶段 3 中,可以将值从 deny 改回 warn,以便接收警告而不是异常。将无法使用 allow 来避免警告。

  • 在阶段 5 中,当所有的内存访问方法都被移除后,--sun-misc-unsafe-memory-access 将被忽略。最终它会被移除。

JDK 中的以下工具可以帮助高级开发人员了解他们的代码如何在 sun.misc.Unsafe 中使用已弃用的方法:

  • 当源代码在 sun.misc.Unsafe 中使用内存访问方法时,javac 会发出弃用警告。每个弃用警告都可以通过在代码中使用 @SuppressWarnings("removal") 来抑制。

  • 当在命令行启用 JDK Flight Recorder (JFR) 时,每当调用一个最终弃用的方法时,都会记录一个 jdk.DeprecatedInvocation 事件。此事件可用于识别 sun.misc.Unsafe 中内存访问方法的使用情况。

    例如,以下是创建 JFR 记录并显示 jdk.DeprecatedInvocation 事件的方法:

    $ java -XX:StartFlightRecording:filename=recording.jfr ...
    $ jfr print --events jdk.DeprecatedInvocation recording.jfr

    当使用 jcmd 或 JFR API 在运行时启动 JFR 时,不会记录此事件。有关此事件及其限制的更多详细信息,请参见相应的 JDK 22 发行说明

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 解决。

在极少数情况下,这些方法会被单独用来检查和操作内存中对象的物理布局(示例见此处)。对于这一使用场景,目前没有支持的替代方案;更多讨论请参见下方

上面的前三种方法在 JDK 18 中已弃用

与这些方法关联的字段也已弃用,准备移除:

  • int INVALID_FIELD_OFFSET
  • int ARRAY_[TYPE]_BASE_OFFSET
  • int ARRAY_[TYPE]_INDEX_SCALE

堆外方法

双峰内存访问方法

迁移示例

堆内存访问

假设类 Foo 有一个我们希望以原子方式加倍的 int 字段。我们可以使用 sun.misc.Unsafe 来实现这一点:

class Foo {

private static final Unsafe UNSAFE = ...; // A sun.misc.Unsafe object

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 API:

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

}

我们可以使用 sun.misc.Unsafe 来对数组元素执行 volatile 写操作:

class Foo {

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 int[] a = new int[10];

public void setVolatile(int index, int value) {
if (index < 0 || index >= a.length)
throw new ArrayIndexOutOfBoundsException(index);
UNSAFE.putIntVolatile(a, ARRAY_BASE + ARRAY_SCALE * index, value);
}

}

我们可以将其改进为使用 VarHandle

class Foo {

private static final VarHandle AVH = MethodHandles.arrayElementVarHandle(int[].class);

private int[] a = new int[10];

public void setVolatile(int index, int value) {
AVH.setVolatile(a, index, value);
}

}

堆外内存访问

下面是一个使用 sun.misc.Unsafe 分配堆外缓冲区并执行三个操作的类:对一个 int 进行易变写入、对缓冲区的一个子集进行批量初始化,以及将缓冲区数据复制到 Java 的 int 数组中:

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

}

我们可以对其进行改进,以使用标准的 ArenaMemorySegment API:

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

}

风险与假设

  • 多年来,sun.misc.Unsafe 中与内存访问无关的方法在引入标准替代方案后已被弃用以供移除,其中许多方法已经被移除:

    我们看到这些相对晦涩的方法的移除对 Java 生态系统的影响很小。然而,内存访问方法更为人所知。此提案假设移除它们将对库产生影响。因此,为了最大限度地提高可见性,我们提议通过 JEP 流程弃用和移除它们,而不是简单地通过 CSR 请求和发行说明。

  • 此提案假设库开发者将从 sun.misc.Unsafe 中不受支持的方法迁移到 java.* 中受支持的方法。

    我们强烈建议库开发者不要从 sun.misc.Unsafe 中不受支持的方法迁移到 JDK 内部其他地方的不受支持的方法。

    忽视此建议的库开发者将迫使他们的用户在命令行上使用 --add-exports--add-opens 选项。这不仅不方便,而且有风险:JDK 内部结构可能会在版本之间发生变化且不另行通知,从而破坏依赖内部结构的库,进而破坏依赖这些库的应用程序。

  • 此提案的一个风险是,某些库以无法通过 JDK 23 中可用的标准 API 复制的方式使用了 sun.misc.Unsafe 的堆内存访问方法。例如,库可能使用 Unsafe::objectFieldOffset 获取对象中字段的偏移量,然后使用 Unsafe::putInt 在该偏移量处写入一个 int 值,而不考虑字段是否为 int 类型。标准的 VarHandle API 无法在如此低的级别检查或操作对象,因为它通过名称和类型引用字段,而不是通过偏移量。

    依赖字段偏移量的用例实际上是在揭示或利用 JVM 的实现细节。我们认为,此类用例不需要由标准 API 支持。

  • 库可能使用 UNSAFE.getInt(array, arrayBase + offset) 来访问堆中的数组元素而无需边界检查。这种惯用法通常用于随机访问,因为通过普通数组索引操作或 MemorySegment API 对数组元素进行顺序访问已经受益于 JIT 的边界检查消除。

    在我们看来,无需边界检查的数组元素随机访问并不是需要由标准 API 支持的用例。通过数组索引操作或 MemorySegment API 进行随机访问相较于 sun.misc.Unsafe 的堆内存访问方法性能略有损失,但在安全性和可维护性方面却有很大的提升。特别是,使用标准 API 可确保在所有平台和所有 JDK 版本上可靠运行,即使将来 JVM 的数组实现发生变化也是如此。

未来工作

在弃用并移除 79 个内存访问方法后,sun.misc.Unsafe 将仅包含三个未被弃用的方法:

  • pageSize,它将被单独弃用并移除。鼓励库开发者通过下调用(downcall)直接从操作系统获取内存页大小。

  • throwException,它也将被单独弃用并移除。此方法曾被 JDK 中的方法用来将受检异常包装为非受检异常,但那些方法,例如 Class::newInstance,现在已被弃用。

  • allocateInstance,它将在中期内作为 sun.misc.Unsafe 中唯一保留的方法。一些序列化库在反序列化时会使用它。提供一个标准的替代方案是一个长期项目。