为什么JUC中大量使用了sun.misc.Unsafe 这个类,但官方却不建议开发者使用?

sun.misc.Unsafe中的方法在实际开发中真的没有应用的场景吗?只适合在官方的SDK中使用?
关注者
159
被浏览
98,566

16 个回答

请让我先戴上“官方角度”的帽子:

对,如果不是开发标准库(java.* / javax.* / sun.*)的话,请不要使用sun.misc.Unsafe。

Unsafe是用于在实质上扩展Java语言表达能力、便于在更高层(Java层)代码里实现原本要在更低层(C层)实现的核心库功能用的。这些功能包括裸内存的申请/释放/访问,低层硬件的atomic/volatile支持,创建未初始化对象等。它原本的设计就只应该被标准库使用。

它可以看作JVM提供了特殊支持的“intrinsics”。请参考我以前做的一个演示稿:

Intrinsic Methods in HotSpot VM

,第11页。(链接是好的,咳咳)

正常的Java程序应该使用标准API,它们对sun.misc.Unsafe“应该”被使用的场景都做了“合理”的包装。

从JDK9开始,Java的新模块化设计将使得非标准库的模块都无法访问到sun.misc.Unsafe。所以就算为了“向未来兼容”也请不要直接使用sun.misc.Unsafe。事实上标准库里也只有jdk.base模块能访问到sun.misc.Unsafe,因为它被设计成了非exported类。

2015-11更新:

Encapsulating internal APIs in JDK 9 (sun.misc.Unsafe, etc.)

为了让开发者有机会过渡到尽量不使用sun.misc.Unsafe,以及让JDK能在内部演化Unsafe API,JDK9目前的计划是会:

  • 默认不允许Java应用代码访问sun.misc.Unsafe类。它将会被移动到 jdk.unsupported 模块中。但是会有一个新的VM参数来控制是否开放对Unsafe的访问。
  • JDK内部将不会继续使用 sun.misc.Unsafe 类,而是会克隆出一个新的 jdk.internal.misc.Unsafe 类来替代前者的功能。新的Unsafe类将完全不暴露给应用,并且会随时根据JDK的需要而演化其API。

===============================================

然后再请让我戴上“外部开发者”的帽子:

我当然知道很多外部开发者过去和现在都在“滥用”sun.misc.Unsafe,来实现各种奇怪的功能。

其中一种是嫌Java性能不够好,例如说数组访问的边界检查语义,嫌这个开销太大,觉得用Unsafe会更快;

另外一种是想要自己实现一些需要裸内存访问的功能,例如(咳咳)Terracotta系的⋯

还有一些纯粹想乱来的,乱搞JVM内部数据结构啥的,例如有时无聊的我。

Oracle的Java开发团队一直在努力提高Java性能,尽可能消除大家因为性能不足而要转向Unsafe的状况。目前正在开发中的VarHandle、Arrays 2.0都是这方面的努力。

嫌性能不好的同学,如果您下定决心要用Unsafe来解决问题,而且做出来确实性能好,那就用呗。但到JDK9之前请先准备好备用方案因为到时候就用不了了。

其它需求的话请不要用Unsafe了⋯真的没啥好处。Terracotta的东西经常JDK升个小版本他们就挂了,为啥,就是因为他们乱依赖JDK某个具体小版本的某个具体实现,人家内部实现经常需要变,哪能那么搞⋯

因为 Java 是一个强调内存安全的语言,它希望尽可能地保证不会出现 Segmentation Fault 等内存不安全的状况。

但现实中没有这么理想,想要高效地实现某些功能不可避免的会使用到内存不安全的功能,为了解决这个矛盾,Java 提供了 Unsafe 这个包含一组不安全的 intrinsics,在内部用这些方法实现,由开发者自行保证内存安全,对外包装成一个内存安全的方法提供给用户。

这些方法内存不安全,违背 Java API 的原则,而且实际上也不是 API 的一部分,没有兼容性保证,但前模块化时代无法将它保护起来,所以很多代码为了高性能而依赖于这个类,JDK 留着它也仅仅是为了和旧代码兼容,并且已经在逐渐删除其中的方法了。

sun.misc.Unsafe中的方法在实际开发中真的没有应用的场景吗?只适合在官方的SDK中使用?

应该说 Unsafe 的功能在高性能以及多线程开发中有应用的场景,但 Unsafe 这个级别的抽象不适合作为公共 API 提供给一般用户。

为了解决这个问题,Java 9 开始 Unsafe 的很多功能都被封装成更高级的安全 API 提供。

Unsafe 比较常用的重要功能大概有以下这几类:

  • 基于对象中字段的 offset,以各种类型的方式(直接读写,易失读写,原子操作等)访问字段的能力;
  • 以各种类型的方式(除了上面提到的,还可以读写不匹配数组元素类型的值)访问数组元素的能力;
  • 对应于 C/C++ 的 malloc/free/memmove 以及指针读写等直接访问本机内存的能力;
  • 内存屏障。

下面我展开说说这几类功能的用途、问题,以及新的替代方案。

访问对象字段

使用 Unsafe 访问对象字段是各种意义上来说极其不安全的事情。

它没有边界检查,传入不正确的 offset 可以轻易损坏对象,进而让整个 JVM 崩溃;

它也不像反射一样会检查访问权限,用户可以用它访问更改无权访问的变量,甚至是写入最终字段,造成一系列的问题。

它的用途是:

  1. 标准库内部有些地方用它代替反射来访问字段。这样用的地方通常是反射的底层实现类,所以没法用反射来代替;
  2. 用来对字段进行原子操作;
  3. 真的要绕开最终字段的限制设置字段。

用途 3 很危险,会因为 JVM 优化而造成异常结果,标准库里也只在序列化的时候这样用,普通用户绝对不该这样。

对于用途 1,正常情况下用户用反射就可以了,而“绕开访问权限”这种反射做不到的事情正是官方想封堵掉的行为。任何代码都能绕开访问权限破坏了平台的安全性和完整性,真正想干这种事请老老实实用 --add-opens 授予对应包的反射权限,或者直接用 jdk.internal.misc.Unsafe 来做。

用途 2 很有用,java.util.concurrent.atomic 类里那堆 AtomicXxx 的底层就使用了 Unsafe 实现。 用 Unsafe 实现可以在字段层面进行原子操作,不用再分配一个 j.u.c.atomic 包里的类的对象实例,这样更节约内存,也能少一层间接引用,更加快速。

对于这个用途,Java 9 引入的 java.lang.invoke.VarHandle 类可以实现相关功能:

public class AtomicReference<V> {
    private static final VarHandle VALUE;

    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private volatile V value;


    public final boolean compareAndSet(V expectedValue, V newValue) {
        return VALUE.compareAndSet(this, expectedValue, newValue);
    }

    public final V getAndSet(V newValue) {
        return (V) VALUE.getAndSet(this, newValue);
    }
}

访问数组元素

Unsafe 有时候也用来访问数组的元素。 通常需要这样访问的目的是实现原子操作,或者优化性能。

AtomicXxxArray 底层原本也是靠它实现的原子操作。这项功能的替代品也是 VarHandle,用 MethodHandles.arrayElementVarHandle 就能拿到数组元素的 Varhandle,现在 AtomicXxxAtomicXxxArray 都已经用 VarHandle 重新实现了。

Unsafe 优化数组读写性能也是一个值得了解的操作。

通常来说 Unsafe 不会比直接访问更快,这里的优化性能体现在读写 byte[] 上。

由于一次性向堆上写入一个 int/long 在很多平台上比写入一个个字节更快,用 Unsafe 直接在 byte[] 上读写 int/long 也是一种优化手段。

Java 9 提供了一个 MethodHandles.byteArrayViewVarHandle 来生成进行此操作的 VarHandle,我们可以这样快速地将四个字节放入 byte[] 中:

VarHandle INT = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN);

void putBytes(byte[] array, byte b0, byte b1, byte b2, byte b3) {
  INT.set(array, 0, ((b0 & 0xff) << 0) | ((b1 & 0xff) << 8) | ((b2 & 0xff) << 16) | ((b3 & 0xff) << 24));
}

访问本机内存

Unsafe 可以用于提供类似 C/C++ 的直接访问任意内存,但是不安全也不好用,只能说提供了这个功能。

Project Panama 实现了全新的更安全和易用的外部内存管理 API 作为替代:

内存屏障

Unsafe 中还有 loadFence/storeFence/fullFence 这三个内存屏障原语, 从 Java 9 开始,VarHandle 类提供了对应的静态 API 方法实现相同的功能。