Java魔法类:Unsafe利用解析

Unsafe是位于sun.misc包下的一个类,次要提供一些用于执行低级别、不平安操作的办法,如间接拜访零碎内存资源、自主治理内存资源等,这些办法在晋升Java运行效率、加强Java语言底层资源操作能力方面起到了很大的作用。但因为Unsafe类使Java语言领有了相似C语言指针一样操作内存空间的能力,这无疑也减少了程序产生相干指针问题的危险。在程序中适度、不正确应用Unsafe类会使得程序出错的概率变大,使得Java这种平安的语言变得不再“平安”,因而对Unsafe的应用肯定要谨慎

注:本文对sun.misc.Unsafe公共API性能及相干利用场景进行介绍。根本介绍如下Unsafe源码所示,Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe办法的类为疏导类加载器所加载时才非法,否则抛出SecurityException异样。

一:根本介绍

如下Unsafe源码所示,Unsafe类为一单例实现,提供静态方法 getUnsafe获取Unsafe实例,当且仅当调用getUnsafe办法的类疏导类加载器所加载时才非法,否则抛出SecurityException异样。

public final class Unsafe {  // 单例对象  private static final Unsafe theUnsafe;  private Unsafe() {  }  @CallerSensitive  public static Unsafe getUnsafe() {    Class var0 = Reflection.getCallerClass();    // 仅在疏导类加载器`BootstrapClassLoader`加载时才非法    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {          throw new SecurityException("Unsafe");    } else {      return theUnsafe;    }  }}

那如若想应用这个类,该如何获取其实例?有如下两个可行计划。

  • 其一,从getUnsafe办法的应用限度条件登程,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相干办法的类A所在jar包门路追加到默认的bootstrap门路中,使得A被疏导类加载器加载,从而通过Unsafe.getUnsafe办法平安的获取Unsafe实例。

    java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相干办法的类所在jar包门路
  • 其二,通过反射获取单例对象theUnsafe

    private static Unsafe reflectGetUnsafe() {  try {    // Unsafe类有个成员变量:private static final Unsafe theUnsafe    Field field = Unsafe.class.getDeclaredField("theUnsafe");    field.setAccessible(true);    return (Unsafe) field.get(null);  } catch (Exception e) {    log.error(e.getMessage(), e);    return null;  }}

    或者:先获取Class对象,再通过结构器创立Unsafe对象

    // 通过反射取得Unsafe对象Class unsafeClass = Unsafe.class;// 取得构造函数,Unsafe的构造函数为公有的Constructor constructor = unsafeClass.getDeclaredConstructor();// 设置为容许拜访公有内容constructor.setAccessible(true);// 创立Unsafe对象Unsafe unsafe = (Unsafe) constructor.newInstance();

二:性能介绍


如上图所示,Unsafe提供的API大抵可分为内存操作CASClass相干对象操作线程调度零碎信息获取内存屏障数组操作等几类,上面将对其相干办法和利用场景进行具体介绍。

1、内存操作

这部分次要蕴含堆外内存的调配拷贝开释给定地址值操作等办法。

//分配内存, 相当于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办法

应用堆外内存的起因
  • 对垃圾回收进展的改善。因为堆外内存是间接受操作系统治理而不是JVM,所以当咱们应用堆外内存时,即可放弃较小的堆内内存规模。从而在GC时缩小回收进展对于利用的影响。
  • 晋升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存堆外内存的数据拷贝操作,对于须要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都倡议存储到堆外内存。

    典型利用

    DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中利用宽泛。DirectByteBuffer对于堆外内存的创立、应用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
    下图为DirectByteBuffer构造函数,创立DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,调配的堆外内存一起被开释。

那么如何通过构建垃圾回收追踪对象Cleaner实现堆外内存开释呢?
Cleaner继承自Java四大援用类型之一的虚援用PhantomReference(家喻户晓,无奈通过虚援用获取与之关联的对象实例,且当对象仅被虚援用援用时,在任何产生GC的时候,其均可被回收),通常PhantomReference与援用队列ReferenceQueue联合应用,能够实现虚援用关联对象被垃圾回收时可能进行零碎告诉、资源清理等性能。如下图所示,当某个被Cleaner援用的对象将被回收时,JVM垃圾收集器会将此对象的援用放入到对象援用中的pending链表中,期待Reference-Handler进行相干解决。其中,Reference-Handler为一个领有最高优先级的守护线程,会循环不断的解决pending链表中的对象援用,执行Cleaner的clean办法进行相干清理工作。

所以当DirectByteBuffer仅被Cleaner援用(即为虚援用)时,其能够在任意GC时段被回收。当DirectByteBuffer实例对象被回收时,在Reference-Handler线程操作中,会调用Cleanerclean办法依据创立Cleaner时传入的Deallocator来进行堆外内存的开释。

2、CAS相干

如下源代码释义所示,这部分次要为CAS相干操作的办法。

/**    *  CAS  * @param o         蕴含要批改field的对象  * @param offset    对象中某field的偏移量  * @param expected  期望值  * @param update    更新值  * @return          true | false  */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);

什么是CAS? 即比拟并替换,实现并发算法时罕用到的一种技术。CAS操作蕴含三个操作数——内存地位预期原值新值。执行CAS操作的时候,将内存地位的值与预期原值比拟,如果相匹配,那么处理器会主动将该地位值更新为新值,否则,处理器不做任何操作。咱们都晓得,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不统一问题,Unsafe提供的CAS办法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

典型利用

CASjava.util.concurrent.atomic相干类Java AQSCurrentHashMap等实现上有十分宽泛的利用。如下图所示,AtomicInteger的实现中,动态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在动态代码块中通过Unsafe的objectFieldOffset办法获取。在AtomicInteger中提供的线程平安办法中,通过字段valueOffset的值能够定位到AtomicInteger对象中value的内存地址,从而能够依据CAS实现对value字段的原子操作。

下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset失去value的内存地址valueAddress=“0x11000c”;而后通过CAS进行原子性的更新操作,胜利则返回,否则持续重试,直到更新胜利为止。

3、线程调度

这部分,包含线程挂起、复原、锁机制等办法。

//勾销阻塞线程public native void unpark(Object thread);//阻塞线程public native void park(boolean isAbsolute, long time);//取得对象锁(可重入锁)@Deprecatedpublic native void monitorEnter(Object o);//开释对象锁@Deprecatedpublic native void monitorExit(Object o);//尝试获取对象锁@Deprecatedpublic native boolean tryMonitorEnter(Object o);

如上源码阐明中,办法parkunpark即可实现线程的挂起与复原,将一个线程进行挂起是通过park办法实现的,调用park办法后,线程将始终阻塞直到超时或者中断等条件呈现;unpark能够终止一个挂起的线程,使其恢复正常。

典型利用

Java锁同步器框架的外围类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupportparkunpark办法理论是调用Unsafeparkunpark形式来实现。

4、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);
典型利用

从Java 8开始,JDK应用invokedynamicVM Anonymous Class联合来实现Java语言层面上的Lambda表达式

  • invokedynamic: invokedynamic是Java 7为了实现在JVM上运行动静语言而引入的一条新的虚拟机指令,它能够实现在运行期动静解析出调用点限定符所援用的办法,而后再执行该办法,invokedynamic指令的分派逻辑是由用户设定的疏导办法决定。
  • VM Anonymous Class:能够看做是一种模板机制,针对于程序动静生成很多构造雷同、仅若干常量不同的类时,能够先创立蕴含常量占位符的模板类,而后通过Unsafe.defineAnonymousClass办法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何ClassLoader上面,只有当该类没有存在的实例对象、且没有强援用来援用该类的Class对象时,该类就会被GC回收。故而VM Anonymous Class相比于Java语言层面的匿名外部类无需通过ClassClassLoader进行类加载且更易回收。
    Lambda表达式实现中,通过invokedynamic指令调用疏导办法生成调用点,在此过程中,会通过ASM动静生成字节码,而后利用UnsafedefineAnonymousClass办法定义实现相应的函数式接口的匿名类,而后再实例化此匿名类,并返回与此匿名类中函数式办法的办法句柄关联的调用点;而后能够通过此调用点实现调用相应Lambda表达式定义逻辑的性能。上面以如下图所示的Test类来举例说明。

    Test类编译后的class文件反编译后的后果如下图一所示(删除了对本文阐明无意义的局部),咱们能够从中看到main办法的指令实现、invokedynamic指令调用的疏导办法BootstrapMethods、及静态方法lambda$main$0(实现了Lambda表达式中字符串打印逻辑)等。在疏导办法执行过程中,会通过Unsafe.defineAnonymousClass生成如下图二所示的实现Consumer接口的匿名类。其中,accept办法通过调用Test类中的静态方法lambda$main$0来实现Lambda表达式中定义的逻辑。而后执行语句consumer.accept("lambda")其实就是调用下图二所示的匿名类的accept办法。

    5、对象操作

    此局部次要蕴含对象成员属性相干操作及非常规的对象实例化形式等相干办法。

    //返回对象成员属性在内存地址绝对于此对象的内存地址的偏移量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润饰的也能通过此办法实例化,只需提类对象即可创立相应的对象。因为这种个性,allocateInstancejava.lang.invoke、Objenesis(提供绕过类结构器的对象生成形式)、Gson(反序列化时用到)中都有相应的利用。

如下图所示,在Gson反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创立实例,否则通过UnsafeAllocator来实现对象实例的结构,UnsafeAllocator通过调用UnsafeallocateInstance实现对象的实例化,保障在指标类无默认构造函数时,反序列化不够影响。

6、数组相干

这部分次要介绍与数据操作相干的arrayBaseOffsetarrayIndexScale这两个办法,两者配合起来应用,即可定位数组中每个元素在内存中的地位。

//返回数组中第一个元素的偏移地址public native int arrayBaseOffset(Class<?> arrayClass);//返回数组中一个元素占用的大小public native int arrayIndexScale(Class<?> arrayClass);
典型利用

这两个与数据操作相干的办法,在java.util.concurrent.atomic包下的AtomicIntegerArray(能够实现对Integer数组中每个元素的原子性操作)中有典型的利用,如下图AtomicIntegerArray源码所示,通过UnsafearrayBaseOffsetarrayIndexScale别离获取数组首元素的偏移地址base单个元素大小因子scale。后续相干原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的getAndAdd办法即通过checkedByteOffset办法获取某数组元素的偏移地址,而后通过CAS实现原子性操作。

7、内存屏障

在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机拜访的操作中的一个同步点,使得此点之前的所有读写操作都执行后才能够开始执行此点之后的操作),防止代码重排序。

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前public native void loadFence();//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前public native void storeFence();//内存屏障,禁止load、store操作重排序public native void fullFence();
典型利用

在Java 8中引入了一种锁的新机制——StampedLock,它能够看成是读写锁的一个改良版本StampedLock提供了一种乐观读锁的实现,这种乐观读锁相似于无锁的操作,齐全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”景象。因为StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不统一问题,所以当应用StampedLock的乐观读锁时,须要听从如下图用例中应用的模式来确保数据的一致性。

如上图用例所示计算坐标点Point对象,蕴含点挪动办法move及计算此点到原点的间隔的办法distanceFromOrigin。在办法distanceFromOrigin中,首先,通过tryOptimisticRead办法获取乐观读标记;而后从主内存中加载点的坐标值 (x,y);而后通过StampedLock的validate办法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存过程中,主内存的值是否已被其余线程通过move办法批改,如果validate返回值为true,证实(x, y)的值未被批改,可参加后续计算;否则,需加乐观读锁,再次从主内存加载(x,y)的最新值,而后再进行间隔计算。其中,校验锁状态这步操作至关重要,须要判断锁状态是否产生扭转,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不统一。

下图为StampedLock.validate办法的源码实现,通过锁标记与相干常量进行位运算、比拟来校验锁状态,在校验逻辑之前,会通过UnsafeloadFence办法退出一个load内存屏障,目标是防止上图用例中步骤StampedLock.validate中锁状态校验运算产生重排序导致锁状态校验不精确的问题。

8、零碎相干

这部分蕴含两个获取零碎相干信息的办法。

//返回零碎指针的大小。返回值为4(32位零碎)或 8(64位零碎)。public native int addressSize();  //内存页的大小,此值为2的幂次方。public native int pageSize();
典型利用

如下图所示的代码片段,为java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法,其依赖于UnsafepageSize办法获取零碎内存页大小实现后续计算逻辑。

三:结语

本文对Java中的sun.misc.Unsafe的用法及利用场景进行了根本介绍,咱们能够看到Unsafe提供了很多便捷、乏味的API办法。即便如此,因为Unsafe中蕴含大量自主操作内存的办法,如若使用不当,会对程序带来许多不可控的劫难。因而对它的应用咱们须要慎之又慎。