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
中的内存访问方法:
- 在 JDK 9 中引入的
java.lang.invoke.VarHandle
(JEP 193)提供了安全高效地操作堆上内存(即对象的字段、类的静态字段和数组的元素)的方法。 - 在 JDK 22 中引入的
java.lang.foreign.MemorySegment
(JEP 454)提供了安全高效地访问堆外内存的方法,有时与VarHandle
合作。
这些标准 API 保证没有未定义行为,承诺长期稳定性,并与 Java 平台的工具和文档具有高质量的集成(其使用示例见下文 迁移示例)。鉴于这些 API 的可用性,现在弃用并最终移除 sun.misc.Unsafe
中的内存访问方法是合适的。
移除 sun.misc.Unsafe
中的内存访问方法是确保 Java 平台具有 默认完整性 的长期协调努力的一部分。其他举措包括对 Java 本地接口(JNI,JEP 472)和代理的动态加载(JEP 451)施加限制。这些努力将使 Java 平台更加安全和高效。它们还将降低应用程序开发人员因在不支持的 API 更改后在新版本上中断的库而陷入旧 JDK 版本的风险。
描述
sun.misc.Unsafe
的内存访问方法可以分为三类:
- 访问堆上内存的方法(堆上),
- 访问堆外内存的方法(堆外),以及
- 访问堆上和堆外内存的方法(双模——双模方法接受一个参数,该参数要么指向堆上的对象,要么为
null
以表示堆外访问)。
我们将分阶段弃用和移除这些方法,每个阶段都在单独的 JDK 功能发布中进行:
弃用所有内存访问方法——堆上、堆外和双模方法,以便移除。这将导致引用这些方法的代码在编译时出现弃用警告,提醒库开发人员它们即将被移除。下面描述的一个新命令行选项将使应用程序开发人员和用户在使用这些方法时收到运行时警告。
与弃用警告不同,自 2006 年以来,
javac
已针对sun.misc.Unsafe
的使用发出警告:warning: Unsafe is internal proprietary API and may be removed in a future release
这些警告将继续发出,并且无法抑制。
当使用内存访问方法时(无论是直接还是通过反射),在运行时发出警告,如下文所述。这将提醒应用程序开发人员和用户这些方法即将被移除,并需要升级库。
当使用内存访问方法时(无论是直接还是通过反射),抛出异常。这将进一步提醒应用程序开发人员和用户这些方法即将被移除。
移除 堆上 方法。这些方法将首先被移除,因为自 2017 年的 JDK 9 以来,它们已经有了标准替代品。
移除 堆外 和 双模 方法。这些方法将稍后移除,因为它们自 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)
这些方法用于获取偏移量或比例,然后与双模方法(下文)结合使用以读取和写入字段或数组元素。这些用例现在通过 VarHandle
和 MemorySegment::ofArray
来解决。
在极少数情况下,这些方法会单独使用来检查和操作内存中对象的物理布局(参见示例 此处)。对于这种用例,目前没有支持的替代方案;有关进一步讨论,请参见 下文。
上面的前三个方法已在 JDK 18 中被弃用。
与这些方法相关联的字段也已被弃用并计划移除:
int INVALID_FIELD_OFFSET
int ARRAY_[TYPE]_BASE_OFFSET
int ARRAY_[TYPE]_INDEX_SCALE
堆外方法
long allocateMemory(long bytes)
— 替换为Arena::allocate
或通过 FFM 向下调用 到 C 库的malloc()
函数long reallocateMemory(long address, long bytes)
— 向下调用到realloc()
void freeMemory(long address)
—Arena::close
或向下调用到free()
void invokeCleaner(java.nio.ByteBuffer directBuffer)
—MemorySegment::asByteBuffer
void setMemory(long address, long bytes, byte value)
—MemorySegment::fill
void copyMemory(long srcAddress, long destAddress, long bytes)
—MemorySegment::copy
[type] get[Type](long address)
—MemorySegment.get(ValueLayout.Of[Type] layout, long offset)
void put[Type](long address, [type] x)
—MemorySegment.set(ValueLayout.of[Type] layout, long offset, [type] value)
long getAddress(long address)
— 替换为MemorySegment.get(ValueLayout.OfAddress layout, long offset)
void putAddress(long address, long x)
— 替换为MemorySegment.set(ValueLayout.ofAddress layout, long offset, MemorySegment value)
int addressSize()
(以及int ADDRESS_SIZE
) — 替换为ValueLayout.ADDRESS.byteSize()
双模式内存访问方法
[type] get[Type](Object o, long offset)
— 替换为VarHandle::get
void put[Type](Object o, long offset, [type] x)
— 替换为VarHandle::set
[type] get[Type]Volatile(Object o, long offset)
— 替换为VarHandle::getVolatile
void put[Type]Volatile(Object o, long offset, [type] x)
— 替换为VarHandle::setVolatile
void putOrdered[Type](Object o, long offset, [type] x)
— 替换为VarHandle::setRelease
[type] getAndAdd[Type](Object o, long offset, [type] delta)
— 替换为VarHandle::getAndAdd
[type] getAndSet[Type](Object o, long offset, [type] newValue)
— 替换为VarHandle::getAndSet
boolean compareAndSwap[Type](Object o, long offset, [type] expected, [type] x)
— 替换为VarHandle::compareAndSet
void setMemory(Object o, long offset, long bytes, byte value)
— 替换为MemorySegment::fill
或Arrays::fill
void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes)
—MemorySegment::copy
或System::arrayCopy
迁移示例
堆内存访问
假设类 Foo
有一个 int
类型的字段,我们希望原子性地将其值加倍。我们可以使用 sun.misc.Unsafe
来实现:
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:
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 写入:
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
:
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
数组中:
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;
}
}
我们可以使用标准的 Arena
和 MemorySegment
API 来改进上述代码:
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 中移除,其中许多方法已被移除:在 JDK 9 引入
java.lang.invoke.MethodHandles.Lookup::defineClass
后,sun.misc.Unsafe::defineClass
在 JDK 11 中被移除。在 JDK 15 引入
MethodHandles.Lookup::defineHiddenClass
后,sun.misc.Unsafe::defineAnonymousClass
在 JDK 17 中被移除。在 JDK 15 引入
MethodHandles.Lookup::ensureInitialized
后,sun.misc.Unsafe::{ensureClass,shouldBe}Initialized
在 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 的数组实现将来发生变化也是如此。
未来工作
在弃用并移除 79 个内存访问方法后,sun.misc.Unsafe
将仅包含三个未弃用的方法:
pageSize
,该方法将单独弃用并移除。鼓励库开发人员通过下调直接从操作系统获取内存页面大小。throwException
,该方法将单独弃用并移除。历史上,JDK 中的方法使用此方法将受检异常包装为非受检异常,但这些方法(例如Class::newInstance
)现已弃用。allocateInstance
,该方法将在中期内保留为sun.misc.Unsafe
中的唯一方法。一些序列化库在反序列化时会使用此方法。提供标准替代方案是一个长期项目。