跳到主要内容

JEP 498: 使用 sun.misc.Unsafe 中的内存访问方法时发出警告

QWen Max 中英对照 JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe

概述

在运行时,当 sun.misc.Unsafe 中的任何内存访问方法首次被调用时发出警告。所有这些不受支持的方法在 JDK 23 中已被彻底弃用。它们已经被标准 API 所取代,即 VarHandle API(JEP 193,JDK 9)和外部函数与内存 API(JEP 454,JDK 22)。我们强烈建议库开发人员从 sun.misc.Unsafe 迁移到受支持的替代方案,以便应用程序能够顺利迁移到现代 JDK 版本。

历史

此JEP是JEP 471(JDK 23)的后续,后者不推荐使用 sun.misc.Unsafe 中的内存访问方法,并描述了在未来版本中逐步移除这些方法的过程。本JEP的目标、非目标、动机以及风险和假设部分与JEP 471基本相同。

目标

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

  • 当开发人员的应用程序直接或间接使用 sun.misc.Unsafe 中的内存访问方法时,通知开发人员。

非目标

  • 不以在使用 sun.misc.Unsafe 类的任何成员时发出警告为目标。它的少数方法不用于内存访问;这些方法将被单独标记为废弃并移除。

动机

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 23 中仍然可以开箱即用。

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

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

移除 sun.misc.Unsafe 中的内存访问方法是确保 Java 平台具有默认完整性的长期协调工作的一部分。其他举措包括对 Java 本机接口(JNI,JEP 472)和动态加载代理程序(JEP 451)施加限制。这些努力将使 Java 平台更加安全、性能更高。它们还将减少应用程序开发人员由于在新版本中更改了不支持的 API 而导致库失效,从而被困在旧 JDK 版本上的风险。

描述

我们正在分阶段弃用并移除 sun.misc.Unsafe 中的内存访问方法:

  1. JDK 23 已弃用所有内存访问方法并计划移除。这导致引用这些方法的代码在编译时会发出弃用警告,提醒库开发者这些方法即将被移除。

  2. 默认情况下,JDK 24 将在首次使用任何内存访问方法时(无论是直接使用还是通过反射)发出警告。也就是说,无论使用了哪些内存访问方法以及每个特定方法被使用了多少次,它最多只会发出一次警告。这将提醒应用程序开发者和用户注意这些方法即将被移除,并需要升级库。警告示例如下:

    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
  • JDK 26 或更高版本将在使用内存访问方法时(无论是直接使用还是通过反射使用)抛出异常。这将进一步提醒应用程序开发人员和用户这些方法即将被移除。

  • 在 JDK 26 之后的版本中,我们将移除自 JDK 9(2017 年)以来已有标准替代方法的内存访问方法。

  • 在 JDK 26 之后的版本中,我们将移除自 JDK 22(2023 年)以来才有标准替代方法的内存访问方法。

完整的内存访问方法及其标准替代方案列表可以在 JEP 471 中找到。

识别 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 允许使用内存访问方法,并且在运行时不会发出任何警告。

    这种模式是 JDK 23 中的默认设置。

  • --sun-misc-unsafe-memory-access=warn 允许使用内存访问方法,但在每次使用这些方法时会发出一条警告,如上文所述。

    这种模式将是 JDK 24 中的默认设置。

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

  • --sun-misc-unsafe-memory-access=deny 禁止使用内存访问方法,每当尝试使用这些方法(无论是直接使用还是通过反射)时,都会抛出一个 UnsupportedOperationException

--sun-misc-unsafe-memory-access 选项的默认值在上述各个阶段的发行版中会有所不同:

  • allow 是阶段 1(JDK 23)中的默认值。

  • warn 将是阶段 2(JDK 24)中的默认值,就像每次调用 java 启动器时都包含 --sun-misc-unsafe-memory-access=warn。在 JDK 24 中可以将值从 warn 改回 allow,从而避免警告。也就是说,可以通过 --sun-misc-unsafe-memory-access=allow 来调用 java 启动器。

  • deny 将是阶段 3(JDK 26 或更高版本)中的默认值。在阶段 3 中,可以将值从 deny 改为 warn,以接收单个警告而不是异常。将不能使用 allow 来避免警告。

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

您还可以使用 JDK Flight Recorder (JFR) 来识别何时使用了内存访问方法。当在命令行上启用 JFR 时,每当调用一个最终废弃的方法时,JVM 就会记录一个 jdk.DeprecatedInvocation 事件。可以使用此事件来识别 sun.misc.Unsafe 中的内存访问方法的使用情况。例如,以下是如何创建一个 JFR 录制并显示 jdk.DeprecatedInvocation 事件:

$ java -XX:StartFlightRecording:filename=recording.jfr -jar myapp.jar
$ jfr print --events jdk.DeprecatedInvocation recording.jfr
shell
$ java -XX:StartFlightRecording:filename=recording.jfr ...
$ jfr print --events jdk.DeprecatedInvocation recording.jfr
jdk.DeprecatedInvocation {
startTime = 11:53:00.196 (2024-11-08)
method = sun.misc.Unsafe.staticFieldOffset(Field)
invocationTime = 11:53:00.174 (2024-11-08)
forRemoval = true
stackTrace = [
Foo.main(String[]) line: 16
...
]
}
$

有关此事件及其限制的更多详细信息,请参阅 JDK 22 发行说明

风险和假设

  • 多年来,在引入标准替代方法后,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 对数组的实现将来发生变化。