
很多人第一次听说Unsafe这个类时第一反应是“这东西真的能用”——一个连名字都在劝退你的 Java 内部类常年栖身于 JDK 的底层却被并发框架、高性能网络库和各类中间件当成宝贝。我最初接触Unsafe是在排查某个诡异的并发问题时顺着堆栈一路摸到了sun.misc.Unsafe从那以后就再也没法无视它了。这篇文章想做的事很简单把Unsafe从底层原理讲到实际用法再讲到你踩坑时该怎么排查。内容尽量贴合实战代码可以直接抄注释也按我会写给同事看的风格来。不管你是想搞懂 ConcurrentHashMap 为什么快还是想在自己项目里安全地用它做点底层优化这篇文章都能给你一个相对完整的视角。1. 初识 Unsafe为什么 Java 里会有这么“不安全”的类1.1 它到底解决的是什么问题Java 向来以内存安全自居JVM 帮你管好了对象生命周期、垃圾回收、类型校验普通开发者根本不需要碰裸内存。但有一批底层框架不这么想——它们要极致的性能、要绕过某些语言层面的限制、要在 Java 的边界上试探 C/C 才能做到的操作。Unsafe就是 JVM 留给这些“疯狂开发者”的后门。它提供了直接读写内存、CAS 原子操作、线程挂起与恢复、内存屏障、绕过构造函数实例化对象等能力。拿CAS来说java.util.concurrent.atomic包下所有原子类底层全是靠Unsafe的compareAndSwap系列方法实现的。ReentrantLock、Semaphore、CountDownLatch这些同步工具最底层也是通过LockSupport.park/unpark来挂起和唤醒线程而LockSupport又直接调用了Unsafe的park/unpark。换句话说你平时写的并发代码不管用没用Lock或者ConcurrentHashMap江水之下早就在跟Unsafe打交道了。1.2 为什么说它“不安全”Unsafe的不安全主要体现在两点。第一它打破 Java 语言的内存安全承诺。你可以像 C 语言一样操作裸内存分配了内存不清空、提前释放了内存、访问悬空指针之后依然正常使用轻则数据错乱重则 JVM 崩溃。JVM 崩溃不像普通异常可以被捕获它是整个进程直接退出。第二它绑定 JVM 内部实现。Unsafe的方法签名和布局不是 Java 语言规范承诺的而是 HotSpot 的实现细节。JDK 版本一升级某些方法可能被改名、废弃或者行为变化。比如 JDK 9 之后模块化系统让很多内部 API 不可见了JDK 15 又正式把sun.misc.Unsafe的部分内存方法标记为弃用。所以业界对Unsafe的态度一直是“能不用就不用非用不可也要封装好、隔离好”。提示Unsafe不是语言规范的一部分它是 HotSpot JVM 的实现细节。只要你还跑在 HotSpot 系 JVM 上比如 Oracle JDK、OpenJDK它就在那里但如果有一天你切换到别的 JVM代码就未必能编译通过了。1.3 谁在用它谁在骂它现实是几乎所有自带并发原语的 Java 框架都在背后用它我基于常见实践帮你梳理了几类使用场景典型例子用 Unsafe 做什么JUC 并发工具AbstractQueuedSynchronizer、原子类CAS 更新状态、park/unpark 线程高性能网络框架Netty、某跨平台通信库内存分配、引用计数更新、避免 CAS 竞争序列化与 ORM某些高性能序列化库绕过构造函数创建对象、直接填充字段类加载与字节码框架某些增强型 ClassLoader动态分配内存、获取字段偏移量容器与中间件某些缓存组件直接操作堆外内存、热点字段存储我见过不少人对Unsafe又爱又恨。爱的是它给了你接近 C 语言的威力恨的是它一旦出问题排查成本极高而且几乎无法靠调试器或日志快速定位。2. 获取入口拿到 Unsafe 对象的三条路径2.1 正统的反射方式推荐Unsafe的单例存在于theUnsafe静态字段中但它的初始化被 JVM 严格控制外部类直接访问会抛SecurityException。常规办法是用反射强行读取import sun.misc.Unsafe; import java.lang.reflect.Field; public class UnsafeAccessor { private static final Unsafe UNSAFE; static { try { Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); UNSAFE (Unsafe) field.get(null); } catch (Exception e) { throw new IllegalStateException(无法获取Unsafe实例, e); } } public static Unsafe getUnsafe() { return UNSAFE; } }有两点需要特别强调setAccessible(true)在 Java 9 之后会受到模块系统限制。如果Unsafe所在的jdk.unsupported模块没有向你的模块开放反射时会抛InaccessibleObjectException。这个我们在后面的常见问题里会展开说。反射获取到的Unsafe是全 JVM 共享的同一个实例它没有私有状态所以无论从哪里拿到行为完全一致。2.2 正统的引导类加载器方式JVM 内部对Unsafe的限制逻辑是只有由引导类加载器Bootstrap ClassLoader加载的类才能直接访问。你可以通过写一个被引导类加载器加载的类来获取但你的业务代码肯定是被应用类加载器加载的这条路通常只在写 JVM 插件或特殊 Agent 时才会走。// 这类代码仅示例实际需要放进引导类加载器可见的路径 public class UnsafeHolder { private static final Unsafe UNSAFE Unsafe.getUnsafe(); }这种方式对绝大多数项目都不适用也不建议为了拿Unsafe去动启动参数风险远大于收益。2.3 获取失败时的常见原因我实际遇到过的场景主要有这么几类失败原因具体表现应对方式模块不允许反射InaccessibleObjectException启动参数加--add-opens java.base/jdk.internal.miscALL-UNNAMED或改用jdk.internal.misc.UnsafeJava 版本太老Field.get时抛SecurityException换用更高版本 JDK或者检查 SecurityManager 配置类被混淆或裁剪找不到theUnsafe字段检查是否对 JDK 类做了额外处理通常不会出现某些 JVM 实现没有该类加载时ClassNotFoundException换回 HotSpot 系 JVM或者用反射兼容方案从我的经验来看你工作中遇到的 99% 情况都能通过第一条反射方案解决。你若还没到写 JVM Agent 的程度专注反射方案就好。3. 底层能力全景Unsafe 四大类核心能力3.1 内存操作让你在 Java 里玩 C 语言Unsafe最强悍也最危险的能力是直接操作堆外内存。核心方法就那么几个// 分配一块指定大小的堆外内存返回内存地址 long address unsafe.allocateMemory(size); // 写入一个 int 值到指定地址 unsafe.putInt(address, 42); // 从指定地址读取一个 int 值 int value unsafe.getInt(address); // 释放堆外内存 unsafe.freeMemory(address); // 将堆内存块复制到另一个地址 unsafe.copyMemory(srcAddress, destAddress, size);这些操作与ByteBuffer.allocateDirect()相比省掉了ByteBuffer的封装和边界检查速度更快、更贴近硬件但你也失去了所有保护。我补几个实战中非常有用的细节堆外内存不受堆大小限制所以可以使用-Xmx以外的内存但必须手动确保释放否则会像 C/C 一样内存泄漏。分配内存尽量对齐比如按 8 字节对齐能避免部分架构下的性能惩罚也能规避某些 CPU 上的非对齐访问异常。释放后立即置空引用否则一旦误用释放的地址相当于是访问了一个悬空指针比内存泄漏还难查。我通常在基于常见实践做堆外缓存时会写一个资源封装类把所有allocateMemory/freeMemory都收敛到构造函数和close()里不让业务方直接接触裸地址。3.2 对象字段偏移量绕过 getter/setter 的秘密通道Unsafe可以告诉你一个对象内部字段在内存布局里的偏移量然后直接按偏移量读写字段值// 拿到字段在对象内存布局中的偏移量 long offset unsafe.objectFieldOffset(SomeClass.class.getDeclaredField(value)); // 不经过构造器、不经过 setter直接写字段 unsafe.putInt(object, offset, 100); // 直接读字段 int current unsafe.getInt(object, offset);这里的关键点在于objectFieldOffset拿到的是“字段相对对象起始地址的偏移量”putInt(object, offset, value)做的事情等同于object.value value但不会触发任何访问控制、不会走代理或字节码增强类。它能带来几个实际收益规避 Java 的可见性检查与代理机制适合给某些序列化框架做底层填充。与非安全代码配合时能达到非常高的读写性能因为它省掉了很多冗余判断。配合 CAS 实现无锁字段更新这是后面要聊的重点。但注意字段偏移量依赖 JVM 对象布局不同版本、不同 GC 参数下的结果可能不同。不要缓存偏移量到磁盘或跨 JVM 传递。3.3 CAS 原子操作并发世界的基石这是Unsafe用得最广、也最值得深入理解的一类能力。// 如果对象 obj 的 offset 偏移处的当前值等于 expect则更新为 update // 成功返回 true失败返回 false boolean casInt unsafe.compareAndSwapInt(obj, offset, expect, update); boolean casLong unsafe.compareAndSwapLong(obj, offset, expect, update); boolean casObject unsafe.compareAndSwapObject(obj, offset, expect, update);CASCompare And Swap是个 CPU 级别的原子指令。它做了这么一件事比较一个内存位置的值和预期值如果相同则写入新值整个比较和交换过程不可被中断。这样就不需要加锁也能实现线程安全的更新。我基于常见实践举个例子实现一个简单地线程安全计数器public class UnsafeCounter { private static final Unsafe UNSAFE UnsafeAccessor.getUnsafe(); private static final long VALUE_OFFSET; static { try { VALUE_OFFSET UNSAFE.objectFieldOffset( UnsafeCounter.class.getDeclaredField(value)); } catch (NoSuchFieldException e) { throw new ExceptionInInitializerError(e); } } private volatile int value; public int incrementAndGet() { int current; do { current UNSAFE.getIntVolatile(this, VALUE_OFFSET); } while (!UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, current, current 1)); return current 1; } }这段代码和AtomicInteger的内部实现几乎一模一样。do-while循环的目的就是处理 CAS 失败后的重试因为只要有竞争就可能有人在两次操作之间修改了value此时需要重新读取再试一次。CAS 真正的价值在于无锁并发下的性能优势。竞争激烈时它会白白消耗 CPU自旋所以不能盲目用但竞争不激烈时它比synchronized轻量得多。注意compareAndSwapInt系列只保证“变量更新”这一步的原子性如果你的业务流程里需要“先检查一堆条件再更新”那么这些检查必须在 CAS 之外做好并发控制否则会出现检查与更新之间的窗口期。3.4 park/unpark线程挂起与唤醒的终极实现LockSupport.park()底层就是Unsafe.park(false, 0L)LockSupport.unpark(thread)底层就是Unsafe.unpark(thread)。// 挂起当前线程直到被 unpark 或被中断 unsafe.park(false, 0L); // 唤醒指定线程 unsafe.unpark(thread);park和wait最本质的区别是park/unpark不依赖对象监视器也不需要抢占锁你不需要先synchronized才能挂起线程同时它有内置的“许可”机制可以先unpark再park线程不会被卡住。这个语义在构建自定义同步器时尤其重要AQS 里大量线程阻塞就是靠这个实现的。你用park时要注意以下几点挂起状态下的线程如果被中断park会直接返回且不抛异常。所以循环条件里要判断中断标志否则会出现“被唤醒但是没有满足唤醒条件”的问题。不要用park(时间)替代定时任务调度它的时间精度和唤醒机制不适合精确调度。unpark一个已终止的线程没有任何效果也不会报错可以放心调用。3.5 内存屏障控制 CPU 重排序的最后一道防线CPU 和编译器为了性能会对指令进行重排序这种重排序单线程下没问题多线程下却可能导致你观察到的执行顺序和代码顺序不一致。volatile能解决一部分问题但如果你想更精细地控制屏障就要用到Unsafe// 读屏障禁止该调用点之前的读操作越过它 unsafe.loadFence(); // 写屏障禁止该调用点之前的写操作越过它 unsafe.storeFence(); // 全屏障读和写都不能越过 unsafe.fullFence();这里我需要给一段直白的解释loadFence可以理解为“在此处立一道墙前面的读操作必须已完成后面的读操作不会提前”。storeFence表示“前面的写操作对其它线程可见后面的写操作不会被提前执行”。fullFence则是全堵死。最典型的应用是某种“自旋状态位”的无锁数据结构。当你用自旋等待某个状态位时如果只依赖volatile读可能在部分弱内存模型处理器上读到旧值此时显式插入loadFence就会让你拿到的永远是较新值。3.6 绕过构造函数创建对象Unsafe.allocateInstance(Class)可以在不调用构造函数、不执行初始化块的情况下直接创建对象。// 分配一个类实例但完全跳过构造函数 SomeClass instance (SomeClass) unsafe.allocateInstance(SomeClass.class);这有两个用途序列化框架恢复对象时不希望触发构造函数里的业务逻辑。某些代理或字节码增强场景希望先拿到空对象再手工填充字段。风险同样很明显对象字段都是默认值如果构造函数里有必要的资源初始化操作跳过它会导致对象处于半初始化状态。我用它时一定会配合严格的对象状态检查机制否则排查问题会想死。3.7 类加载与常量池操作Unsafe还支持defineClass、defineAnonymousClass和shouldBeInitialized等能力可以动态定义类。这几个操作深到令人发指但对于写框架的人来说是真实需要的。.lambda表达式的底层实现就依赖了类似匿名类定义的能力。普通业务开发几乎不应该直接碰这些方法容易把自己写崩溃。4. 实战用 Unsafe 实现一个轻量级堆外存储理论光说不行我基于实战中的常见玩法写一个可以直接参考的完整例子用Unsafe实现一个简单的堆外键值存储存储的是int数组数据。4.1 需求界定我们不取复杂的哈希表实现只做一个“定长槽位”的堆外存储。比如要存 10000 个long值每个值占用 8 字节那么总共就是 80000 字节的堆外内存。这样的优点是没有 GC 压力数据不参与堆内存回收适合缓存场景。4.2 代码实现import sun.misc.Unsafe; import java.lang.reflect.Field; /** * 基于 Unsafe 的极简堆外 Long 数组存储。 * 演示目的堆外分配、写入、读取、释放以及边界检查。 */ public class OffHeapLongArray implements AutoCloseable { private static final Unsafe UNSAFE; private static final int LONG_BYTES 8; static { UNSAFE getUnsafe(); } /** * 反射获取 Unsafe 实例 */ private static Unsafe getUnsafe() { try { Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { throw new RuntimeException(获取Unsafe失败, e); } } private final long address; private final int capacity; public OffHeapLongArray(int capacity) { this.capacity capacity; // 分配 capacity * 8 字节堆外内存 this.address UNSAFE.allocateMemory((long) capacity * LONG_BYTES); // 可选把内存清零避免读到旧数据 UNSAFE.setMemory(address, (long) capacity * LONG_BYTES, (byte) 0); } public void set(int index, long value) { checkIndex(index); // 写入 long使用 putLong UNSAFE.putLong(address (long) index * LONG_BYTES, value); } public long get(int index) { checkIndex(index); return UNSAFE.getLong(address (long) index * LONG_BYTES); } private void checkIndex(int index) { if (index 0 || index capacity) { throw new IndexOutOfBoundsException(index index , capacity capacity); } } Override public void close() { // 释放堆外内存注意幂等性 UNSAFE.freeMemory(address); } }使用示例public class Demo { public static void main(String[] args) { try (OffHeapLongArray array new OffHeapLongArray(10)) { array.set(0, 100L); array.set(9, 999L); System.out.println(array.get(0)); // 100 System.out.println(array.get(9)); // 999 } } }4.3 每个关键点背后的考虑内存清零setMemory是模拟 C 语言calloc的行为。不清零的话新分配的内存可能是随机值你get时可能读到一堆垃圾数据。虽然可以用“先写后读”避免但清零更保险。边界检查Unsafe的裸内存访问不帮你做任何边界检查。如果你越界写入了相邻地址那不是抛一个异常而是可能直接损坏别的数据甚至触发 JVM 崩溃。手动checkIndex是必须的。释放的幂等性close()如果被调用两次第二次freeMemory就释放了一个无效地址这在我自己的实践里是真实踩过的坑。更稳妥的做法是在释放前判断地址是否为 0释放后把地址置 0Override public void close() { if (address ! 0) { UNSAFE.freeMemory(address); // 防止重复释放 } }但由于address是final的要改成“非 final 置零”的模式。实际项目里我一般会用一个独立的volatile boolean closed标志来做幂等private volatile boolean closed; Override public void close() { if (closed) { return; } closed true; UNSAFE.freeMemory(address); }4.4 一个更贴合并发的变体如果你不想每次计算address index * 8可以在初始化时一次性计算好每个索引的绝对地址存到一个普通 Java 数组里读取时直接按索引取地址再访问速度会比重复乘法运算略快。这种微优化在大规模数据访问时意义会更明显。5. 常见问题与排查技巧实录5.1 不同 JDK 版本下 Unsafe 的兼容性Unsafe在不同版本中的变化是实际项目中最大的不稳定因素。我基于常见实践整理了一份对比JDK 版本主要变化实践建议JDK 8sun.misc.Unsafe全量可用无模块限制直接使用sun.misc.UnsafeJDK 9 及以后模块系统上线jdk.unsupported模块管理部分内部 API需要--add-exports或改用反射JDK 15部分内存方法标记弃用运行时注意告警日志提前评估替换方案JDK 17强封装 JDK 内部 API 趋势明显尽可能在业务层封装隔离如果你的项目因为 JDK 升级导致Unsafe拿不到可以尝试在 JVM 启动参数添加--add-opens jdk.unsupported/sun.miscALL-UNNAMED如果是通过反射获取theUnsafe不同模块的开放规则有差异实践中不一定每次都要加参数先跑一下再根据报错决定即可。注意不要过分依赖--add-opens来解决所有问题这只能让你拿到实例如果某个方法本身行为在更高版本里变了照样会出现诡异问题。5.2 Unsafe 抛出的异常为什么那么诡异Unsafe的方法大多是 native 方法很多不抛普通异常而是返回一个false或者直接让 JVM 崩溃。我列几个典型症状症状可能原因排查思路程序启动即崩溃拿到 hs_err 日志内存越界写入或者释放无效内存看 hs_err 文件里的当前线程栈定位分配和释放点数据偶发错乱没有异常并发访问同一个堆外地址或者对象字段偏移量算错用-XX:UseSerialGC等切换 GC 观察是否变化加日志验证地址反射获取时报InaccessibleObjectException模块不通按 5.1 的--add-opens处理GC 时间异常升高可能把堆外内存地址写成了引用导致 GC 扫描一片脏内存检查代码里是否误用putObject而非putLong进程内存持续增长堆外分配后没有释放用内存分析工具查看 native 内存重点查allocateMemory调用点5.3 park 后线程无法唤醒的排查顺序这类问题常出现在自己写同步器的时候排查顺序很有讲究先确认是不是在park前就把unpark许可消耗掉了导致后续park不等unpark就立即返回或永久阻塞。再看是不是检查中断标志时出了问题park被中断返回后如果循环没有判断Thread.interrupted()会再次立即挂起看起来像醒不了。最后确认线程是否真的进入了WAITING状态用jstack看线程栈顶是不是在Unsafe.park处。大多数情况下问题不是park本身而是调用方的逻辑没有把“唤醒条件”和“挂起操作”串联好。5.4 独家避坑技巧在实际项目里拿Unsafe搞事情有几个通用的避坑技巧我花了很长时间才总结出来所有操作都包在服务类里不要让业务代码直接持有Unsafe否则以后想换实现或加监控都难。统一封装后你可以轻易地在里面加入指标计数、日志、安全检查。地址值不要轻易用long存成常量带到别处堆外地址只有在相应的生命周期内有效你把它存进 Redis 或者文件里下次启动再读那就是一颗定时炸弹。用getIntVolatile/putIntVolatile时字段必须声明为volatile这两个方法本身提供的是 volatile 语义的读写如果字段本身不是 volatile可能出现不一致的语义别在类型系统上搞混淆。处理内存泄漏优先排查ByteBuffer和Unsafe分配点我见过很多内存问题不是常规-Xmx堆造成的而是堆外内存泄漏。用Native Memory Tracking (NMT)加-XX:NativeMemoryTrackingsummary能看到每个分类的 native 内存占用非常有用。提示在排查堆外内存时可以启动参数加-XX:NativeMemoryTrackingdetail来获取更细的分配调用栈但注意它会带来一定的性能损耗线上开启需谨慎评估。5.5 线上问题复盘案例基于常见实践某个模拟项目里某服务的堆外缓存不断增长几乎每周都要重启。排查中发现该缓存组件在物理上用了Unsafe.allocateMemory分配存储位置但部分数据在写入失败时并没有把地址放进“待释放队列”导致内存无法回收。当时修复方案很简单把“分配地址”和“释放地址”全部纳入统一的生命周期管理谁分配谁负责释放写入失败也要走统一释放流程。加了两行代码内存曲线就平稳了。这种问题的本质不是Unsafe不好而是业务代码对异常路径考虑不足。Unsafe不会帮你兜底它把生命周期管理的责任完全交给你。6. 关于需要不需要用 Unsafe 的几个灵魂拷问6.1 什么时候真的该用我基于自己的经验把“该用”的场景圈得很窄需要堆外内存做超大数据缓存或 IO 缓冲区且目标内存可能超过堆大小的合理范围。需要最大性能的并发原语且已经证明 JUC 工具在当前场景下是瓶颈。需要绕过 Java 语法限制完成“非正统对象初始化”比如高性能序列化。你在写框架、中间件、底层库目的是给别人提供基础能力。如果你不是这四种情况就不要碰。业务代码里为了快几微秒去用Unsafe在工程上往往得不偿失。6.2 什么时候绝对别用我也总结几条经典的“绝对别用”场景想通过直接操作内存来“优化”一个没有明显性能问题的普通方法。准备把某个简单 POJO 的字段用putLong绕过 setter 来写仅仅因为“看起来酷”。没有对应的异常处理、没有内存释放逻辑就贸然在上线代码里写allocateMemory。项目运行环境非常多样不同 JVM、不同平台且没有能力在每个环境都做完整验证。一个很好判断的标准如果你写完Unsafe相关代码后自己都觉得“这里可能以后会出问题”那以后它真的会出问题。6.3 替代方案JDK 官方给你的路Unsafe虽然功能独特但一部分能力其实有官方替代Unsafe 能力官方替代品适合场景CAS 操作VarHandleJava 9需要原子操作且不想依赖sun.misc堆外内存ByteBuffer.allocateDirect简单堆外缓冲不需要极低延迟字段偏移量绕过构造器sun.reflect.ReflectionFactory序列化框架专有路径内存屏障VarHandle的acquire/release需要精细内存语义的高级并发park/unparkLockSupport线程挂起和唤醒VarHandle是我比较建议现代项目优先考虑的方案。它提供了类似Unsafe的原子操作、内存屏障语义却是在语言规范内定义的类型安全更好行为可预测性也更强。6.4 未来趋势Unsafe 会消失吗业内对Unsafe的淘汰讨论很多。JDK 官方一直在推进封装内部 API 和提供替代方案但短期内Unsafe不会完全消失因为大量现存框架已经依赖它直接移除会引发连锁反应。更可能的路径是框架层渐渐从Unsafe迁移到VarHandle和其他官方 APIUnsafe则像安全带一样慢慢退居幕后最终只保留给 JVM 自身和底层库使用。作为开发者不要为Unsafe的“暂时可用”而沾沾自喜还是要紧跟 JDK 版本升级步伐尽量把代码收敛到官方支持的 API 上。7. 几个真正写得漂亮的封装模式7.1 对业务屏蔽 Unsafe 的封装层我见到过比较成熟的框架对Unsafe的封装通常是这样组织的一个内部类专门管理所有Unsafe方法的调用。所有内存生命周期方法都要登记和释放。对外暴露的是业务语义比如“存储一个配置项”“创建一个高性能计数器”而不是“分配一段内存”。public class NativeMemoryManager { private static final Unsafe UNSAFE UnsafeAccessor.getUnsafe(); private final ListLong allocatedAddresses new CopyOnWriteArrayList(); public long allocate(int size) { long addr UNSAFE.allocateMemory(size); allocatedAddresses.add(addr); return addr; } public void free(long addr) { if (allocatedAddresses.remove(addr)) { UNSAFE.freeMemory(addr); } else { throw new IllegalStateException(重复释放或非法地址: addr); } } public void printDebugInfo() { System.out.println(当前堆外分配次数: allocatedAddresses.size()); } }这种封装虽然简单但它能解决 80% 的“内存泄漏和重复释放”问题。因为所有分配和释放都要经过同一处登记你可以很快定位是哪一块业务逻辑没有释放。7.2 对 Unsafe 的兼容层策略高明一点的框架会对Unsafe做版本适配。比如优先用jdk.internal.misc.Unsafe拿不到再用sun.misc.Unsafe再配合反射回退保证在不同 JDK 版本下的可用性。这个思路值得参考public final class UnsafeProvider { private static final String SUN_MISC_UNSAFE sun.misc.Unsafe; private static final String JDK_INTERNAL_UNSAFE jdk.internal.misc.Unsafe; public static Object getUnsafeLike() { Object unsafe tryJDKInternal(); if (unsafe null) { unsafe trySunMisc(); } return unsafe; } private static Object tryJDKInternal() { try { Class? clazz Class.forName(JDK_INTERNAL_UNSAFE); return clazz.getDeclaredField(THE_UNSAFE).get(null); } catch (Exception e) { return null; } } private static Object trySunMisc() { try { Class? clazz Class.forName(SUN_MISC_UNSAFE); Field field clazz.getDeclaredField(theUnsafe); field.setAccessible(true); return field.get(null); } catch (Exception e) { return null; } } }上层的业务代码全部改成通过接口或包装类访问这样做的好处是将来某个版本完全不支持sun.misc.Unsafe了你只需要修改一个文件。8. 写在最后的几句经验之谈Unsafe是个非常有魅力的类它让 Java 拥有了接近底层的能力也让 Java 失去了部分“安全”的滤镜。我喜欢把它比作在游泳池边开了一个通向深海的闸口偶尔去探险很刺激但如果你每天都在那里进出必须有完整的潜水装备和应急预案。在实际项目里我一般把Unsafe圈定在非常小的内部模块里所有调用点必须回答三个问题这段逻辑用语言规范内的 API 能实现吗如果Unsafe的某个方法在下个版本被移除了替换成本是多少这块内存或这个对象的生命周期由谁负责异常路径下能保证释放吗凡是三个问题中有一个答不上来我就会退回去用更保守的方案。一次是某次实现一个底层数据结构时临时代码写得飞快结果因为少释放了一次堆外内存导致服务在高峰期内存飙升最后回滚后老老实实把生命周期管理补全才彻底解决问题。如果你刚接触Unsafe建议从LockSupport的角度切入去理解线程调度从AtomicInteger的内部源码去理解 CAS再把堆外内存的封装模型吃透。沿着这条路走你不仅能驾驭它还能在用VarHandle等替代方案时更游刃有余。最后一个实用的小技巧在任何使用Unsafe的项目里启动参数加上 GC 日志和 Native 内存跟踪平时你没感觉出了问题它就是救命的线索。等到线上故障时才想起补通常已经来不及了。