Java 语言的一大特点就是跨平台,并且提供的有一套完美的内存管理机制。但这都是 JVM 提供的,如果我们想要直接访问系统内存资源、自主管理内存资源等就无法实现。于是 Java 又提供了一个魔法类:Unsafe。
Unsafe 类位于 sun.misc 包中。从名字看,这个类就是一个不安全的类,实际上它确实是封装了一些不安全的操作!
Unsafe 类和 String 类一样的被定义为 final,也就是说它不可以被继承。并且 Unsafe 被设计成了单例,构造函数是私有的,只能通过 getUnsafe 方法获得它。除此之外,getUnsafe 方法还设置了限制条件,只有授信的代码才能获得该类的实例。哪些是授信的代码呢?当然是 JDK 库里面的类是可以随意使用的。
说了半天,这个类,我们无法使用,你讲它又何意义?
别急,Java 虽然不建议我们使用它,但是我们还是可以通过两种方式来使用它。
第一种方式是:让我们的代码在启动时“授信”。在运行程序时,指定 bootclasspath 选项,让你使用 Unsafe 实例的类被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。
这个做法比较少用,所以推荐大家采用第二种方法:通过反射来使用它。
注意有的 IDE 可能支持的不是很友好。比如:eclipse 显示”Access restriction…”错误,但如果你运行代码,它将正常运行。如果这个错误提示令人烦恼,可以通过以下设置来避免:
Unsafe 有 8 大功能,很多号主只讲了它的 CAS 功能。
如上图所示,Unsafe 提供的 105 个 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等。今天我先来说两个大功能:CAS 和内存操作(和我前面的《手把手教你通过Java代码体验强引用、软引用、弱引用、虚引用的区别》、《90%的程序员可能都不了解的堆外内存》都有些关联,这是一个系列)。
CAS 操作主要涉及到下面 3 个 API。
CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。
CAS 在 java.util.concurrent.atomic 相关类、Java AQS、CurrentHashMap 等实现上有非常广泛的应用。比如,在 AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 Unsafe 的 objectFieldOffset 方法获取。在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset 的值可以定位到 AtomicInteger 对象中 value 的内存地址,从而可以根据 CAS 实现对 value 字段的原子操作。
比如,下图就为某个 AtomicInteger 对象自增操作前后的内存示意图,对象的基地址 baseAddress=“0x110000”,通过 baseAddress+valueOffset 得到 value 的内存地址 valueAddress=“0x11000c”;然后通过 CAS 进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。
说完 CAS,我们再来说说 Unsafe 的内存操作。
内存操作主要有下面 9 个 API。
在《手把手教你通过Java代码体验强引用、软引用、弱引用、虚引用的区别》和《90%的程序员可能都不了解的堆外内存》两篇文章中,我已经讲过了。在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。
使用堆外内存的原因是:
对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
我前面提到的 DirectByteBuffer,在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
上图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。具体的释放就是我前面讲的 PhantomReference 虚引用。