JEP 498: 使用 sun.misc.Unsafe 中的内存访问方法时发出警告
概述
历史
此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
中内存访问方法的安全且高效的替代品:
-
java.lang.invoke.VarHandle
,在 JDK 9 中引入(JEP 193),提供了安全高效地操作堆内内存的方法,即对象的字段、类的静态字段和数组的元素。 -
java.lang.foreign.MemorySegment
,在 JDK 22 中引入(JEP 454),提供了安全高效地访问堆外内存的方法,有时会与VarHandle
协同工作。
这些标准 API 确保没有未定义行为,承诺长期稳定性,并与 Java 平台的工具和文档高质量集成。(它们的使用示例见 JEP 471。)鉴于这些 API 的可用性,现在应该弃用并最终移除 sun.misc.Unsafe
中的内存访问方法。
描述
我们正在分阶段弃用并移除 sun.misc.Unsafe
中的内存访问方法:
-
JDK 23 已弃用所有内存访问方法并计划移除。这导致引用这些方法的代码在编译时会发出弃用警告,提醒库开发者这些方法即将被移除。
-
默认情况下,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
$ 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
中与内存访问无关的方法已被弃用并移除,其中许多已经被移除:-
sun.misc.Unsafe::defineClass
在 JDK 9 引入了java.lang.invoke.MethodHandles.Lookup::defineClass
后,在 JDK 11 中被移除。 -
sun.misc.Unsafe::defineAnonymousClass
在 JDK 15 引入了MethodHandles.Lookup::defineHiddenClass
后,在 JDK 17 中被移除。 -
sun.misc.Unsafe::{ensureClass,shouldBe}Initialized
在 JDK 15 引入了MethodHandles.Lookup::ensureInitialized
后,在 JDK 22 中被移除。 -
sun.misc.Unsafe
中的六个杂项方法在有标准替代方法可用后,在 JDK 22 中被标记为即将移除。
这些相对不常见的方法被移除后,对 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 对数组的实现将来发生变化。