深入理解Unsafe类

Unsafe 类位于 sun.misc 包中,sun.misc 包本身在工作中就是个很少被用到的包。在 Java 的发展中,sun.misc 包是 Sun 公司早年的内部工具包,提供了很多底层操作系统级别的方法调用,拥有很大的权限。然而,大多数开发手册都不推荐使用 sun.misc 包,因为直接使用 sun.misc 包下的类,可能会带来安全风险和不可控性。

还记得 Java 和 C 语言相比有什么优势吗?

Java 中是没有指针的。在程序中维护 C 语言指针的经历一定曾让你焦头烂额,而 Java 语言中避免了这种指针操作,这就使得编码的安全性、效率得到大大地提升。

现在,Java 通过 Unsafe 保留了对指针的操作能力。这看上去有点前后矛盾,好像说不要指针的是 Java,说要指针的也是 Java。然而,那么多优秀框架底层都用了 Unsafe,那自然是有它适合的场景。

接下来,我们就来讲讲 Unsafe 类的创建和它的两个常见的应用场景。

创建 Unsafe

我们先来查看一下 Unsafe 的源码。

csharp 复制代码
public finalclass Unsafe {
privatestaticfinal Unsafe theUnsafe;
  ......
private Unsafe() {
  }
@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
      thrownew SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

getUnsafe 似乎可以直接获取一个 Unsafe 对象,然而实际调用后,getUnsafe 方法一定会抛出 SecurityException 异常。这是因为 isSystemDomainLoader 方法会对调用者的 ClassLoader 进行检查,如果调用者的 ClassLoader 不是 BootStrap ClassLoader,调用者就会抛出 SecurityException 异常。

也就是说,只有 JDK 自己的类才可以使用 getUnsafe 来获取 Unsafe 实例,我们工程师自己的方法是没有权限调用 getUnsafe 方法的。

这种情况下,我们如何获取 Unsafe 实例呢?这里有两个方案,我们来一起看一下。

**方案一,利用反射。**在 Unsafe 的源码中,有一个 Unsafe 类型的成员变量------theUnsafe,我们可以通过反射来直接获取这个变量。

ini 复制代码
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);

因为 theUnsafe 是 private 修饰的,所以我们可以直接用 setAccessible 强制打开访问权限,这样就绕开了层层封锁,可以直接获取 Unsafe 对象了。

方案二,我们可以强制把我们的类放入 BootStrap ClassLoader 的 classpath。JDK 提供了-Xbootclasspath/a 命令允许我们把自己写的类加入 BootStrap ClassLoader 路径。这样就可以直接通过上面的 getUnsafe 方法获取 Unsafe 对象了。

千辛万苦创建了 Unsafe 之后,我们来继续看看 Unsafe 的使用场景。由于 Unsafe 的主要功能是管理内存,因此我们就来一起看看,Unsafe 是如何实现内存操作和内存屏障的。

内存操作

JVM 强大的一点功能是内存的自动管理,可以实现对象的自动回收。然而,一些特殊场景,如 NIO 的直接内存,并没有走 JVM 的自动内存管理。Unsafe 允许我们像 C 语言那样使用指针直接操作内存,它的 API 如下:

java 复制代码
public native long allocateMemory(long bytes);
public native long reallocateMemory(long address, long bytes);
public native void setMemory(Object o, long offset, long bytes, byte value);
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
public native void freeMemory(long address);

其中,allocateMemory 是分配内存空间,reallocateMemory 方法可以重新调整内存空间大小,setMemory 可以设置内存的值,copyMemory 和 freeMemory 分别是拷贝和清除。这些方法和 C 语言几乎是对应的。

我们来看一个具体的例子吧。运行这段代码,会输出什么呢?

csharp 复制代码
long addr = unsafe.allocateMemory(4);
unsafe.setMemory(null,addr ,size,(byte)1);
System.out.println(unsafe.getInt(addr));

输出的是 16843009。为什么会这样呢?

首先,unsafe.allocateMemory(4) 分配了一个 4 字节的空间,setMemory 则以 addr 为开始,以 addr+size 为结尾,向每个字节分别写入 1,这时候的内存空间是这样的:

getInt 方法会把结果转成 10 进制并返回,也就是 16843009。

需要注意的是,allocateMemory 分配的是堆外内存,是没有办法自动 GC 的,此时我们只能手动调用 freeMemory 方法才可以释放内存。对于上面的代码,我们可以在 finally 语句块中调用 freeMemory 来释放 addr。

csharp 复制代码
finally {
        unsafe.freeMemory(addr);
        }

使用堆外内存有什么好处呢?

第一个显而易见的好处是减少了 GC。数据放在堆外内存,就和 GC 毫无关系了。

其次,提升了 I/O 操作的性能。我们读取文件或网络数据的时候,不可避免地需要在操作系统内存和 JVM 内存之间拷贝数据。虽然拷贝数据的这个过程是透明的,但占用了一定时间,直接使用堆外内存则减少了一次不必要的内存复制工作,进而提升了 I/O 整体性能。我们熟知的 DirectByteBuffer 底层就是基于 Unsafe 实现的。

内存屏障

接下来,我们再来看看 Unsafe 类在内存屏障场景中的应用。

说到内存屏障,我们就不得不提"指令重排序"了。在多线程中,"指令重排序"是一个经常被提到的概念,简单来说,就是操作系统在保证输出结果正确的情况下,对你的代码执行顺序进行调整,以提升系统执行性能。"指令重排序"的弊端在于它可能导致 CPU Cache 和内存中的数据不一致。

而内存屏障是制止重排序的指令,当然"指令重排序"的目标是为了优化执行性能,如果二话不说直接制止"指令重排序"也是不推荐的。只有当"指令重排序"影响正确结果的情况下,我们才去制止它。Unsafe 提供了下面 3 个内存屏障 API,你看一下:

csharp 复制代码
public native void loadFence();
public native void storeFence();
public native void fullFence();

从名字上看,loadFence 作用于 JVM 的 Load 汇编指令,storeFence 作用于 JVM 的 Store 汇编指令,而 fullFence 同时会对 Load 和 Store 生效。对 JVM 汇编指令没有了解的同学可能认为 Load 就是读操作,Store 就是写操作。

对于这 3 个 API,我们用个形象的比喻来说明一下它们的作用吧。假设你要去做核酸检测,此时排起了长队,不时还出现插队现象,让人不堪其扰。于是,你在队伍中堆起了一堵高大的墙,墙两边的人依然会出现插队现象,但墙一边的人无法到达另一边,这就是屏障的作用。

换成更专业的表述就是屏障是一个同步点,使得同步点前的操作必然在同步点后的操作执行,同时屏障会使得 CPU Cache 中的数据失效,强制指令走内存读取数据。Java 中的 StampedLock 读写锁,就是使用了内存屏障来实现的。

总结

我们介绍了 Unsafe 的基本概念和创建方法,并讲了内存操作和内存屏障两个场景。通过这节课的学习,相信大家可以发现,Unsafe 能给我们带来实实在在的好处。当然,Unsafe 如同它的名称一样,存在不安全的隐患。然而,直到现在 Unsafe 依然存在。这说明,在正确使用的情况下,Unsafe 一定是利大于弊的。

最后讲一句,不到万不得已,不要轻易使用 Unsafe。我们讲解 Unsafe 是为了让大家对底层原理的理解更加深入透彻,至于在生产中应用 Unsafe,还要三思而后行。

相关推荐
东阳马生架构21 分钟前
商品中心—7.自研缓存框架的技术文档
java
林太白24 分钟前
Rust-连接数据库
前端·后端·rust
bug菌38 分钟前
CAP定理真的是死结?业务系统到底该怎么取舍!
分布式·后端·架构
林太白1 小时前
Rust认识安装
前端·后端·rust
掘金酱1 小时前
🔥 稀土掘金 x Trae 夏日寻宝之旅火热进行ing:做任务赢大疆pocket3、Apple watch等丰富大礼
前端·后端·trae
xiayz1 小时前
引入mapstruct实现类的转换
后端
Java微观世界1 小时前
深入解析:Java中的原码、反码、补码——程序员的二进制必修课
后端
不想说话的麋鹿1 小时前
《NestJS 实战:RBAC 系统管理模块开发 (四)》:用户绑定
前端·后端·全栈
Java水解2 小时前
JavaScript 正则表达式
javascript·后端
前端付豪2 小时前
微信支付风控系统揭秘:交易评分、实时拦截与行为建模全流程实战
前端·后端·架构