首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Java Unsafe详解

1 概述

本文基于JDK1.8。

Unsafe类位于rt.jar包,Unsafe类提供了硬件级别的原子操作,类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。由此提供了一些绕开JVM的更底层功能,可以提高程序效率。

JNI:Java Native Interface。使得Java 与 本地其他类型语言(如C、C++)直接交互。

Unsafe 是用于扩展 Java 语言表达能力、便于在更高层(Java 层)代码里实现原本要在更低层(C 层)实现的核心库功能用的。这些功能包括直接内存的申请/释放/访问,低层硬件的 atomic/volatile 支持,创建未初始化对象,通过偏移量操作对象字段、方法、实现线程无锁挂起和恢复等功能。

所谓Java对象的“布局”就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java对象的某个字段。

Unsafe作用可以大致归纳为:

内存管理,包括分配内存、释放内存等。

非常规的对象实例化。

操作类、对象、变量。

自定义超大数组操作。

多线程同步。包括锁机制、CAS操作等。

线程挂起与恢复。

内存屏障。

2 API详解

Unsafe中一共有82个public native修饰的方法,还有几十个基于这82个public native方法的其他方法,一共有114个方法。

2.1 初始化方法

我们可以直接在源码里面看到,Unsafe是单例模式的类:

从上面的代码知道,好像是可以通过getUnsafe()方法获取实例,但是如果我们调用该方法会得到一个异常:

实际上我们可以看到getUnsafe()方法上有个@CallerSensitive注解,就是因为这个注解,在执行时候需要做权限判断:只有由主类加载器(BootStrapclassLoader)加载的类才能调用这个类中的方法(比如rt.jar中的类,就可以调用该方法,原因从类名可以看出来,它是“不安全的”,怎能随意调用,至于有哪些隐患后面会讲)。显然我们的类是由AppClassLoader加载的,所以这里直接抛出了异常。

因此最简单的使用方式是基于反射获取Unsafe实例,代码如下:

2.2 类、对象和变量相关方法

主要包括基于偏移地址获取或者设置变量的值、基于偏移地址获取或者设置数组元素的值、class初始化以及对象非常规的创建等。

2.2.1 对象操作

2.2.2 class 相关

2.2.3 数组元素相关

2.3 内存管理

该部分包括了(分配内存)、(重新分配内存)、(拷贝内存)、(释放内存 )、(获取内存地址)、(获取内存地址指向的整数)、(获取内存地址指向的整数,并支持volatile语义)、(将整数写入指定内存地址)、(将整数写入指定内存地址,并支持volatile语义)、(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。

利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。

Unsafe分配的内存,不受的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露,可以通过方法手动回收;非法的地址访问时,会导致JVM崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM延迟)时,可以使用它,因为直接内存的效率会更好,详细介绍可以去看看Java的NIO源码,NIO中使用了这一技术。

JDK nio包中通过方法分配直接内存时,DirectByteBuffer的构造函数中就使用到了Unsafe的allocateMemory和setMemory方法:通过分配内存、进行内存初始化,而后构建一个虚引用Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放(通过在Cleaner中调用方法)。

2.4 多线程同步

主要包括监视器锁定、解锁以及CAS相关的方法。这部分包括了等方法。其中已经被标记为deprecated,不建议使用。

Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。

2.5 线程的挂起和恢复

这部分包括了park、unpark等方法。

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

Java8的新锁StampedLock使用该系列方法。

2.6 内存屏障

这部分包括了等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。如果你了解JVM的volatile、锁的内存寓意,那么理解“内存屏障”这几个字应该不会太难,这里只是把它包装成了Java代码。

loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理表示该方法之前的所有store操作在内存屏障之前完成。表示该方法之前的所有load、store操作在内存屏障之前完成。

2.7 其他

3 应用

3.0 根据偏移量(指针)修改属性值

3.1 对象的非常规实例化

我们通常所用到的创建对象的方式,有直接new创建、也有反射创建,其本质都是调用相应的构造器,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。

而Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。并且它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。

由于这种特性,allocateInstance在(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。在Gson反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创建实例,否则通过UnsafeAllocator来实现对象实例的构造,UnsafeAllocator通过调用Unsafe的allocateInstance实现对象的实例化,保证在目标类无默认构造函数时,反序列化不够影响。

推荐:Java进阶视频资源

案例:

注意:UNSAFE测试时,其vip字段并没有获取到值。实际上一个new操作,编译成指令后()是3条:

第一条指令的意思是根据类型分配一块内存区域

第二条指令是把第一条指令返回的内存地址压入操作数栈顶

第三条指令是调用类的构造函数,对字段进行显示初始化操作。

Unsafe.allocateInstance()方法只做了第一步和第二步,即分配内存空间,返回内存地址,没有做第三步调用构造函数。所以Unsafe.allocateInstance()方法创建的对象都是只有初始值,没有默认值也没有构造函数设置的值,因为它完全没有使用new机制,直接操作内存创建了对象。

推荐:Java进阶视频资源

3.2 超长数组操作

前面讲的arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。putByte和getByte则可以获取指定位置的byte数据。

常规Java的数组最大值为,但是使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。

下例创建分配一段连续的内存(数组),它的容量是Java允许最大容量的两倍(有可能造成JVM崩溃):

3.3 包装受检异常为运行时异常

3.4 运行时动态创建类

标准的动态加载类的方法是(在编写jdbc程序时,记忆深刻),使用Unsafe也可以动态加载java 的class文件。操作方式就是将文件读取到字节数据组中,并将其传到defineClass方法中。

3.5 实现浅克隆

使用直接获取内存的方式实现浅克隆。把一个对象的字节码拷贝到内存的另外一个地方,然后再将这个对象转换为被克隆的对象类型。为了表述方便,用S代表要克隆的对象,D表示克隆后的对象,SD表示S的内存地址,DD表示D的内存地址,SIZE表示该对象在内存中的大小。

获取原对象的所在的内存地址SD。

计算原对象在内存中的大小SIZE。

新分配一块内存,大小为原对象大小SIZE,记录新分配内存的地址DD。

从原对象内存地址SD处复制大小为SIZE的内存,复制到DD处。

DD处的SIZE大小的内存就是原对象的浅克隆对象,强制转换为源对象类型就可以了。

4 总结和注意

从上面的介绍中,我们可以看到Unsafe非常强大和有趣的功能,但是实际上官方是不推荐我们在代码中直接使用Unsafe类的。甚至从命名就能看出来"Unsafe"——那肯定就是不安全的意思啦。那么什么不安全呢?我们知道C或C++是可以直接操作指针的,指针操作是非常不安全的,这也是Java“去除”指针的原因。

回到Unsafe类,类中包含大量操作指针偏移量的方法,偏移量要自己计算,如若使用不当,会对程序带来许多不可控的灾难,JVM直接崩溃亏。因此对它的使用我们需要慎之又慎,生产级别的代码就更不应该使用Unsafe类了。

另外Unsafe类还有很多自主操作内存的方法,这些都是直接内存,而使用的这些内存不受JVM管理(无法被GC),需要手动管理,一旦出现疏忽很有可能成为内存泄漏的源头。

尽管Unsafe是“不安全的”,但是它的“应用”却很广泛。Unsafe在JUC(java.util.concurrent)包中大量使用(主要是CAS),在netty中方便使用直接内存,还有一些高并发的交易系统为了提高CAS的效率也有可能直接使用到Unsafe,比如Hadoop、Kafka、akka。

总而言之,Unsafe类是一把双刃剑。

热爱技术才能学好技术

每天进步一点点

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230129A03KXB00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券