跳到主要内容

JEP 498:在 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 基本相同。

目标

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

  • 当应用程序直接或间接使用中的内存访问方法时通知开发人员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可以在没有 API 开销的情况下对字段执行 CAS(比较和交换)操作java.util.concurrent.atomic,而sun.misc.Unsafe::setMemory可以操作堆外内存而不受 2GB 的限制java.nio.ByteBuffer。依赖于ByteBuffer操作堆外内存的库(如 Apache Hadoop 和 Cassandra)通过sun.misc.Unsafe::invokeCleaner及时释放堆外内存来提高效率。

不幸的是,并非所有库都会在调用内存访问方法之前认真执行安全检查,因此应用程序存在故障和崩溃的风险。由于从在线论坛轻松复制粘贴,因此这些方法的某些使用是不必要的。其他方法的使用可能会导致 JVM 禁用优化,从而导致性能比使用普通 Java 数组更差。尽管如此,由于内存访问方法的使用非常广泛,因此在 JDK 9( JEP 260sun.misc.Unsafe )中没有与其他低级 API 一起封装。它在 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 及更高版本中,您可以通过使用新的命令行选项运行来评估所使用的库如何受到这些方法的弃用和删除的影响。此选项在精神和形式上与 JDK 9 中JEP 261引入的选项--sun-misc-unsafe-memory-access={allow|warn|debug|deny}类似。其工作原理如下:--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 中将值从 恢复为warnallow从而避免出现警告。也就是说,java可以使用 调用启动器--sun-misc-unsafe-memory-access=allow

  • deny将成为阶段 3(JDK 26 或更高版本)中的默认值。在第 3 阶段,可以将值从 恢复为 ,deny以便warn收到单个警告而不是异常。将无法使用它allow来避免警告。

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

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

$ 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.UnsafeJDK 中其他地方不受支持的方法。

    忽略此建议的库开发人员将强制其用户在命令行上使用--add-exports--add-opens选项运行。这不仅不方便,而且有风险:JDK 内部结构可能会在未通知的情况下从一个版本更改为另一个版本,从而破坏依赖于内部结构的库,进而破坏依赖于库的应用程序。

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

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

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

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