为什么JUC中大量使用了sun.misc.Unsafe 这个类,但官方却不建议开发者使用?
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 崩溃;
它也不像反射一样会检查访问权限,用户可以用它访问更改无权访问的变量,甚至是写入最终字段,造成一系列的问题。
它的用途是:
- 标准库内部有些地方用它代替反射来访问字段。这样用的地方通常是反射的底层实现类,所以没法用反射来代替;
- 用来对字段进行原子操作;
- 真的要绕开最终字段的限制设置字段。
用途 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
,现在 AtomicXxx
和 AtomicXxxArray
都已经用 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 方法实现相同的功能。