JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法以进行移除
总结
目标
-
为在未来 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 年,没有任何方法可以阻止 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 22 中仍然开箱即用。
在过去的几年中,我们引入了两个标准 API,它们是 sun.misc.Unsafe
中内存访问方法的安全且高效的替代品:
-
java.lang.invoke.VarHandle
,在 JDK 9 中引入(JEP 193),提供了安全高效地操作堆内存的方法,即对象的字段、类的静态字段和数组的元素。 -
java.lang.foreign.MemorySegment
,在 JDK 22 中引入(JEP 454),提供了安全高效地访问堆外内存的方法,有时会与VarHandle
协同工作。
这些标准 API 保证没有未定义行为,承诺长期稳定性,并且与 Java 平台的工具和文档有高质量的集成(其使用的示例见下方)。鉴于这些 API 的可用性,现在适当地弃用并最终移除 sun.misc.Unsafe
中的内存访问方法。
描述
sun.misc.Unsafe
的内存访问方法可以分为三类:
- 访问堆内存的方法(on-heap),
- 访问堆外内存的方法(off-heap),以及
- 访问堆内存和堆外内存的方法(bimodal — 一种双模式方法,其参数要么引用堆上的对象,要么为
null
以表示堆外访问)。
我们将分阶段弃用并移除这些方法,每个阶段都在一个单独的 JDK 特性发布中进行:
-
弃用所有内存访问方法 —— 堆内、堆外和双模式 —— 以进行移除。这将导致引用这些方法的代码在编译时出现弃用警告,提醒库开发者它们即将被移除。下面描述的一个新的命令行选项将使应用程序开发者和用户在使用这些方法时收到运行时警告。
与弃用警告不同,
javac
自 2006 年以来一直在对使用sun.misc.Unsafe
发出警告:警告:Unsafe 是内部专有 API,将来可能会删除
这些警告将继续发出,并且无法被抑制。
-
当使用内存访问方法时(无论是直接使用还是通过反射使用),在运行时发出警告,详见下文。这将提醒应用程序开发者和用户这些方法即将被移除,并需要升级库。
-
当使用内存访问方法时(无论是直接使用还是通过反射使用),抛出异常。这将进一步提醒应用程序开发者和用户这些方法即将被移除。
-
移除 堆内 方法。这些方法将首先被移除,因为自 2017 年 JDK 9 以来已经有标准的替代方案。
-
移除 堆外 和 双模式 方法。这些方法将稍后移除,因为自 2023 年 JDK 22 以来才有标准的替代方案。
至于时间安排,我们计划实施
- 第一阶段,通过此 JEP,在 JDK 23 中;
- 第二阶段,发出运行时警告,在 JDK 25 或之前;
- 第三阶段,默认抛出异常,在 JDK 26 或之后;以及
- 第四和第五阶段,将在 JDK 26 之后的版本中移除这些方法。
如果时机成熟,且条件允许,我们可以同时实施第 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: 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
--sun-misc-unsafe-memory-access
选项的默认值将在我们逐步经历上述阶段时随着每个版本的发布而改变:
-
allow
是阶段 1 中的默认值,就好像每次调用java
启动器都包含--sun-misc-unsafe-memory-access=allow
一样。 -
warn
将是阶段 2 中的默认值。在阶段 2 中,可以将值从warn
改回allow
,从而避免警告。 -
deny
将是阶段 3 中的默认值。在阶段 3 中,可以将值从deny
改回warn
,以便接收警告而不是异常。将无法使用allow
来避免警告。 -
在阶段 5 中,当所有的内存访问方法都被移除后,
--sun-misc-unsafe-memory-access
将被忽略。最终它会被移除。
JDK 中的以下工具可以帮助高级开发人员了解他们的代码如何在 sun.misc.Unsafe
中使用已弃用的方法:
-
当源代码在
sun.misc.Unsafe
中使用内存访问方法时,javac
会发出弃用警告。每个弃用警告都可以通过在代码中使用@SuppressWarnings("removal")
来抑制。 -
当在命令行启用 JDK Flight Recorder (JFR) 时,每当调用一个最终弃用的方法时,都会记录一个
jdk.DeprecatedInvocation
事件。此事件可用于识别sun.misc.Unsafe
中内存访问方法的使用情况。例如,以下是创建 JFR 记录并显示
jdk.DeprecatedInvocation
事件的方法:$ 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 = ...; // A sun.misc.Unsafe object
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
进行易变写入、对缓冲区的一个子集进行批量初始化,以及将缓冲区数据复制到 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 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
,它将被单独弃用并移除。鼓励库开发者通过下调用(downcall)直接从操作系统获取内存页大小。 -
throwException
,它也将被单独弃用并移除。此方法曾被 JDK 中的方法用来将受检异常包装为非受检异常,但那些方法,例如Class::newInstance
,现在已被弃用。 -
allocateInstance
,它将在中期内作为sun.misc.Unsafe
中唯一保留的方法。一些序列化库在反序列化时会使用它。提供一个标准的替代方案是一个长期项目。