Skip to content
欢迎扫码关注公众号

JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe | 在 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 内部的调用者会在使用前进行详尽的安全检查,最终会向 Java 平台添加该功能的安全标准 API。

然而,由于在 2002 年无法阻止 sun.misc.Unsafe 在 JDK 之外的使用,其内存访问方法成为了希望获得比标准 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 并未像其他低级 API 一样在 JDK 9(JEP 260)中被封装。它在 JDK 23 中仍然开箱可用,等待安全支持的替代品出现。

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

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

移除 sun.misc.Unsafe 中的内存访问方法是确保 Java 平台默认具有完整性的一项长期协调努力的一部分(JEP 8305968)。其他举措还包括对 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
  3. JDK 26 或之后

    • 每当内存访问方法被调用时(无论是直接调用还是通过反射),会抛出异常。这将进一步提醒应用开发者和用户这些方法即将被移除。
  4. JDK 26 之后的版本

    • 移除自 JDK 9(2017 年)以来已有标准替代方法的内存访问方法。
    • 移除自 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 选项的默认值从一个发行版到另一个发行版会发生变化:

  • 在第 1 阶段(JDK 23),默认值为 allow
  • 在第 2 阶段(JDK 24),默认值将变为 warn,就好像每次调用 java 启动器都包含 --sun-misc-unsafe-memory-access=warn。在 JDK 24 中可以将该值从 warn 恢复为 allow,从而避免警告。
  • 在第 3 阶段(JDK 26 或之后),默认值将变为 deny。在第 3 阶段中,可以从 deny 恢复为 warn 以接收单次警告而不是异常。但是不再可能使用 allow 来避免警告。
  • 在第 5 阶段,当所有内存访问方法都被移除后,--sun-misc-unsafe-memory-access 将被忽略,并最终会被移除。

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

bash
$ 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 未来对数组的实现发生了变化。