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类是一把双刃剑。
热爱技术才能学好技术
每天进步一点点
领取专属 10元无门槛券
私享最新 技术干货