深度理解java语言中的Unsafe类
什么是Unsafe
Unsafe这个类的全限定名是sun.misc.Unsafe,顾名思义,”Unsafe”就是不安全的意思,平时我们不会用到它。
它位于jdk中的rt.jar包里面,主要提供了一些硬件级别的原子性操作,Unsafe类里面的方法都是native方法,它们使用JNI的方式访问本地C++实现库。因此Unsafe类主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。
Unsafe做操作的是直接内存区,所以该类没有办法通过HotSpot的GC进行回收,需要进行手动回收,因此在使用此类时需要注意内存泄漏(Memory Leak)和内存溢出(Out Of Memory)。
在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
通过源码我们可以发现Unsafe类是”final”的,不允许继承。而且构造函数是private的。
private Unsafe() {
}
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
因为该类在sun.misc包下,默认是被BootstrapClassLoader加载的。如果我们在程序中去调用这个类的话,我们使用的类加载器肯定是AppClassLoader。同时我们可以发现在代码里面Unsafe限制了它的 ClassLoader,如果这个方法的调用实例不是由BootClassLoader加载的,则会报SecurityException错误。
那如若想使用这个类,该如何获取其实例?有如下两个可行方案:
- 从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
- 通过反射获取单例对象theUnsafe。
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
System.out.println(unsafe);
Unsafe类的主要方法介绍

内存操作
下面是堆外内存的分配、拷贝、释放、给定地址值操作等方法
//分配内存, 相当于C++的malloc函数 public native long allocateMemory(long bytes); //扩充内存 public native long reallocateMemory(long address, long bytes); //释放内存 public native void freeMemory(long address); //在给定的内存块中设置值 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); //获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的) public native byte getByte(long address); //为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的) public native void putByte(long address, byte x);
通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。
CAS操作
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
compareAndSwapObject方法:比较对象obj中偏移量为offset的变量的值是否与expected相等,相等则使用update值更新,然后返回true,否则返回false。
CAS的全称是Compare And Swap 即比较交换。
执行函数:CAS(V,E,N)
包含3个参数
V表示要更新的变量
E表示预期值
N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。
线程调度
线程调度中提供的方法包括:线程的挂起,恢复和对象锁机制等,其中获取对象的监视器锁方法已经被标记为弃用。主要方法如下
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
————————————————
版权声明:本文为CSDN博主「Anumbrella」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Anumbrella/java/article/details/107050739
如上源码说明中,方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。
JDK中rt,jar下面的LockSupport工具类就是使用Unsafe类实现的。而整个并发框架中对线程的挂起操作被封装在LockSupport类中,LockSupport 类中有各种版本 pack 方法,但最终都调用了Unsafe.park()方法。
Class相关
此部分主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验并确保初始化等。
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的 public native long staticFieldOffset(Field f); //获取一个静态类中给定字段的对象指针 public native Object staticFieldBase(Field f); //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。 public native boolean shouldBeInitialized(Class c); //检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 public native void ensureClassInitialized(Class c); //定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者 public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); //定义一个匿名类 public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
对象操作
Unsafe类中提供了多个方法来进行对象实例化和获取对象的偏移地址的操作
//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量 public native long objectFieldOffset(Field f); //获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //从对象的指定偏移量处获取变量的引用,使用volatile的加载语义 public native Object getObjectVolatile(Object o, long offset); //存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义 public native void putObjectVolatile(Object o, long offset, Object x); //有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效 public native void putOrderedObject(Object o, long offset, Object x); //绕过构造方法、初始化代码来创建对象 public native Object allocateInstance(Class cls) throws InstantiationException;
常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过new机制来实现对象的创建。但是,new机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
非常规的实例化方式:而Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
数组相关
数组操作主要有两个方法
//返回数组中第一个元素的偏移地址 public native int arrayBaseOffset(Class arrayClass); //返回数组中一个元素占用的大小 public native int arrayIndexScale(Class arrayClass);
这两个与数据操作相关的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用。
比如获取数组array的起始位置baseOffset,获取每个数组元素大小indexScale,计算索引为 i 的元素即为
baseOffset + i*indexScale。
nt baseOffset = unsafe.arrayBaseOffset(array.getClass()); int indexScale = unsafe.arrayIndexScale(array.getClass()); baseOffset + i*indexScale
内存屏障
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。内存屏障有两个作用:阻止屏障两侧的指令重排序;强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。在Unsafe中提供了三个方法来操作内存屏障
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //内存屏障,禁止load、store操作重排序 public native void fullFence();
系统相关
获取系统相关信息的方法如下
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。 public native int addressSize(); //内存页的大小,此值为2的幂次方。 public native int pageSize();
在 java.nio下的Bits类中调用了pagesize()方法计算系统中页大小
private static int pageSize = -1;
static int pageSize() { if (pageSize == -1) pageSize = unsafe().pageSize(); return pageSize; }