Skip to content

JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal | 弃用 sun.misc.Unsafe 中的内存访问方法以进行删除

摘要

弃用 sun.misc.Unsafe 中的内存访问方法,以便在将来的版本中移除。这些不受支持的方法已被标准 API 所取代,即 VarHandle API(JEP 193,JDK 9)和外部函数与内存 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 年没有办法阻止在 JDK 外部使用 sun.misc.Unsafe,因此其内存访问方法成为了库开发人员的便捷工具,他们希望获得比标准 API 更多的功能和性能。例如,sun.misc.Unsafe::compareAndSwap 可以在不增加 java.util.concurrent.atomic API 开销的情况下对字段执行 CAS(比较并交换)操作,而 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 的内存访问方法可以分为三类:

  • 访问堆上内存的方法(堆上),
  • 访问堆外内存的方法(堆外),以及
  • 访问堆上和堆外内存的方法(双模——双模方法接受一个参数,该参数要么指向堆上的对象,要么为 null 以表示堆外访问)。

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

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

    与弃用警告不同,自 2006 年以来,javac 已针对 sun.misc.Unsafe 的使用发出警告:

    warning: Unsafe is internal proprietary API and may be removed in a future release

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

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

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

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

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

关于时间安排,我们计划

  • 通过本 JEP 在 JDK 23 中实现第 1 阶段;
  • 在 JDK 25 或之前实现第 2 阶段,即发出运行时警告;
  • 在 JDK 26 或更高版本中默认抛出异常,即实现第 3 阶段;
  • 在 JDK 26 之后的版本中移除这些方法,即实现第 4 和第 5 阶段。

如果时机合适,我们可能会同时实现第 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: sun.misc.Unsafe中的一个最终弃用的方法已被调用
WARNING: sun.misc.Unsafe::setMemory 已被 com.foo.bar.Server 调用(file:/tmp/foobarserver/thing.jar)
WARNING: 请考虑将此问题报告给 com.foo.bar.Server 的维护者
WARNING: sun.misc.Unsafe::setMemory 将在未来的版本中移除

--sun-misc-unsafe-memory-access 选项的默认值将随着我们上述阶段的进行而在每个版本中发生变化:

  • 在第 1 阶段,默认值为 allow,就好像每次调用 java 启动器时都包含了 --sun-misc-unsafe-memory-access=allow

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

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

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

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

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

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

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

    shell
    $ 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 来实现:

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

java
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 写入:

java
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

java
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 的 volatile 写入、缓冲区的一个子集的批量初始化,以及将缓冲区数据复制到一个 Java 的 int 数组中:

java
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 来改进上述代码:

java
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 中与内存访问无关的方法在引入标准替代方法后已被弃用,并将从 JDK 中移除,其中许多方法已被移除:

    从 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,该方法将单独弃用并移除。鼓励库开发人员通过下调直接从操作系统获取内存页面大小。

  • throwException,该方法将单独弃用并移除。历史上,JDK 中的方法使用此方法将受检异常包装为非受检异常,但这些方法(例如 Class::newInstance)现已弃用。

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