bigywater.tech
Java · 398

Unsafe 的历史与替代方案

sun.misc.Unsafe 正在被淘汰。这篇用真实运行回答:Unsafe 在新 JDK 上到底会发生什么,哪些东西它做不到,以及 VarHandle 与 MemorySegment / Arena 这些替代方案在性能和安全性上是否能顶上。

WARNING
JDK 27 默认调用 Unsafe.allocateMemory 时的行为(JDK 17 / 21 无警告;--sun-misc-unsafe-memory-access=deny 抛 UnsupportedOperationException)
0.47–0.51 ns
long[]、Unsafe、MemorySegment、DirectByteBuffer、VarHandle 读写 100 多万个 long 的每元素成本
412 vs 20 MiB
Unsafe 不 freeMemory 与 Arena.ofConfined 在 100 块 4 MiB 后的 RSS 增量

JDK 17 / 21 / 27 上的 Unsafe

实测 · 本机执行输出
  [JDK 17] allocateMemory/putLong/getLong:getLong = 42
  [JDK 21] allocateMemory/putLong/getLong:getLong = 42
  [JDK 27] allocateMemory/putLong/getLong:getLong = 42 | WARNING: A terminally deprecated method in sun.misc.Unsafe has been called | WARNING: sun.misc.Unsafe::allocateMemory has been called by Main (file:<jtmp>/j258_27_unsafe/out/) | WARNING: Please consider reporting this to the maintainers of class Main | WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
  [JDK 27 --sun-misc-unsafe-memory-access=allow] allocateMemory/putLong/getLong:getLong = 42
  [JDK 27 --sun-misc-unsafe-memory-access=warn] allocateMemory/putLong/getLong:getLong = 42 | WARNING: A terminally deprecated method in sun.misc.Unsafe has been called | WARNING: sun.misc.Unsafe::allocateMemory has been called by Main (file:<jtmp>/j258_27_unsafe/out/) | WARNING: Please consider reporting this to the maintainers of class Main
  [JDK 27 --sun-misc-unsafe-memory-access=debug] allocateMemory/putLong/getLong:getLong = 42 | WARNING: sun.misc.Unsafe::allocateMemory called by Main (file:<jtmp>/j258_27_unsafe/out/) | at Main.main(Main.java:9) | WARNING: sun.misc.Unsafe::putLong called by Main (file:<jtmp>/j258_27_unsafe/out/)
  [JDK 27 --sun-misc-unsafe-memory-access=deny] Exception in thread "main" java.lang.UnsupportedOperationException: allocateMemory | at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccessSlow(Unsafe.java:1822) | at jdk.unsupported/sun.misc.Unsafe.beforeMemoryAccess(Unsafe.java:1787) | at jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:679)

objectFieldOffset 的限制

实测 · 本机执行输出
  [JDK 17]
      普通类的字段:objectFieldOffset = 12
      record 的字段:UnsupportedOperationException:can't get field offset on a record class: private final int Main$R.x
      lambda(隐藏类)的捕获字段:UnsupportedOperationException:can't get field offset on a hidden class: private final int Main$$Lambda$1/0x0000008001005000.arg$1
      VarHandle 读 record 字段 x = 7
      VarHandle 写 record 字段:UnsupportedOperationException:null
      VarHandle 对普通 volatile 字段 compareAndSet(0, 5) = true,值 5
  [JDK 21]
      普通类的字段:objectFieldOffset = 12
      record 的字段:UnsupportedOperationException:can't get field offset on a record class: private final int Main$R.x
      lambda(隐藏类)的捕获字段:UnsupportedOperationException:can't get field offset on a hidden class: private final int Main$$Lambda/0x000000f001006800.arg$1
      VarHandle 读 record 字段 x = 7
      VarHandle 写 record 字段:UnsupportedOperationException:set
      VarHandle 对普通 volatile 字段 compareAndSet(0, 5) = true,值 5
  [JDK 27]
      普通类的字段:objectFieldOffset = 16
      record 的字段:UnsupportedOperationException:can't get field offset on a record class: private final int Main$R.x
      lambda(隐藏类)的捕获字段:UnsupportedOperationException:can't get field offset on a hidden class: private final int Main$$Lambda/0x00001ff001042000.arg$1
      VarHandle 读 record 字段 x = 7
      VarHandle 写 record 字段:UnsupportedOperationException:set
      VarHandle 对普通 volatile 字段 compareAndSet(0, 5) = true,值 5

读写成本

实测 · 本机执行输出
  long[](堆内数组)                             写 + 读各 1048576 个 long:每个元素约 0.48 ns(7 轮中位数)
  Unsafe(堆外地址)                             写 + 读各 1048576 个 long:每个元素约 0.51 ns(7 轮中位数)
  MemorySegment.get(JAVA_LONG, offset)     写 + 读各 1048576 个 long:每个元素约 0.48 ns(7 轮中位数)
  MemorySegment 的 VarHandle                写 + 读各 1048576 个 long:每个元素约 0.47 ns(7 轮中位数)
  DirectByteBuffer.getLong                 写 + 读各 1048576 个 long:每个元素约 0.49 ns(7 轮中位数)
  LongBuffer.get(i)                        写 + 读各 1048576 个 long:每个元素约 0.49 ns(7 轮中位数)
  byte[] + byteArrayViewVarHandle          写 + 读各 1048576 个 long:每个元素约 0.50 ns(7 轮中位数)

CAS

实测 · 本机执行输出
  1 线程 AtomicLong.incrementAndGet     每次约 1.9 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  1 线程 VarHandle.getAndAdd            每次约 4.0 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  1 线程 VarHandle.compareAndSet 循环     每次约 8.3 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  1 线程 Unsafe.compareAndSwapLong 循环   每次约 8.2 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  1 线程 synchronized + long++          每次约 5.6 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  1 线程 LongAdder.increment            每次约 5.0 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 AtomicLong.incrementAndGet     每次约 17.0 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 VarHandle.getAndAdd            每次约 43.5 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 VarHandle.compareAndSet 循环     每次约 113.4 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 Unsafe.compareAndSwapLong 循环   每次约 92.8 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 synchronized + long++          每次约 20.5 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)
  4 线程 LongAdder.increment            每次约 1.3 ns(= 总耗时 ÷ 总次数 20000000,5 轮中位数)

堆外内存的生命周期

实测 · 本机执行输出
  Unsafe.allocateMemory 100 × 4 MiB 并写满、不 freeMemory:RSS 42 MiB → 453 MiB;System.gc() 之后 455 MiB(增加 412 MiB)
  Arena.ofConfined 分配 100 × 4 MiB 并写满、每次 close:RSS 42 MiB → 峰值 61 MiB → 结束 63 MiB(增加 20 MiB)
  Arena.ofAuto 同样 100 × 4 MiB、不 close:RSS 63 MiB → 461 MiB;System.gc() 之后 317 MiB(增加 253 MiB)
  访问已 close 的 Arena 的 MemorySegment:IllegalStateException:Already closed
  越界读 MemorySegment:IndexOutOfBoundsException:Out of bound access on segment MemorySegment{ kind: native, address: 0x7990fe6cf0, byteSize: 16 }; attempting to access an element of length 8 at offset 16 which is outside the valid range 0 <= offset+length < byteSize (=16)
  对照:Unsafe 释放后再 getLong 不会抛异常(这里不执行,避免崩溃)

分步演示

Unsafe 的替代方案
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 17.0.20、21.0.12(只测 Unsafe 与 VarHandle)、27(MemorySegment / Arena 是正式 API);MemorySegment 测试用 JDK 27 的 java.lang.foreign;计数器用 System.nanoTime 的 5 轮中位数,不是 JMH
    • 没有验证:Unsafe 的 park / unpark / defineClass / allocateInstance 等其它方法、JDK 22 / 23 / 24 / 25 / 26 的中间行为(JEP 498 的逐版本变化)、JMH 下的结论、Unsafe 在 Netty / Kryo / Hazelcast 等库里的使用与迁移情况、JNI / Linker API(java.lang.foreign.Linker)
    京ICP备2026067502号-1京公网安备11010502063487号