• Unsafe概述


    前言

    ​ 如果看过Java并发包里面的源码、或者netty网络通讯相关源码,就会到Unsafe类有一定的了解。这个与其的特性有关。

    介绍

    ​ **Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。**但是过度使用、不正确使用会适得其反,所以慎用。

    1、基本使用

    Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

    ​ 所以如果要获取实例,有以下这两种方法:

    1)设置调用方法类被引导类加载器加载。

    getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

    java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相关方法的类所在jar包路径 
    
    • 1
    2)通过反射获取单例对象theUnsafe。
    private static Unsafe reflectGetUnsafe() {
        try {
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          field.setAccessible(true);
          return (Unsafe) field.get(null);
        } catch (Exception e) {
          log.error(e.getMessage(), e);
          return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Unsafe功能介绍

    ​ **Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。**通过其所提供的服务,就发现很多地方都用到。

    img

    1、操作内存

    主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。

    ​ 通常,在Java程序中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。

    与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。

    API接口
    //分配内存, 相当于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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    使用堆外内存的原因

    1)对垃圾回收停顿的改善。

    ​ 由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。

    2)提升程序I/O操作的性能。

    通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

    应用

    DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。

    ​ 创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。

    垃圾回收追踪对象Cleaner实现堆外内存释放

    虚引用主要用来跟踪对象被垃圾回收的活动。需要使用ReferenceQueue类和PhantomReference类来实现假性通知。Cleaner继承自PhantomReference,用来实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。

    img

    ​ 所以当某个被Cleaner引用的对象将被回收时,JVM垃圾收集器会将此对象的引用放入到对象引用中的pending链表中,等待Reference-Handler进行相关处理。其中,Reference-Handler为一个拥有最高优先级的守护线程,会循环不断的处理pending链表中的对象引用,执行Cleaner的clean方法进行相关清理工作。

    因此,无法通过虚引用获取与之关联的对象实例(堆外内存),且当对象仅被虚引用引用时,在任何发生GC的时候,其均可被回收。

    内存简易结构图

    在这里插入图片描述

    2、CAS操作

    ​ **CAS即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。**执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

    说根本,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

    API接口
    /**
    	*  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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    应用

    在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。

    ​ **AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。**在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

    3、线程调度

    包括线程挂起、恢复、Lock的锁机制等方法。

    API接口
    //取消阻塞线程
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。

    应用
    典型应用

    Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。

    4、Class相关

    主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

    API接口
    //获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    源码分析
    //1、注册native方法,是的Unsafe类可以操作C语言 
     private static native void registerNatives(); 
     static { 
         registerNatives(); 
         sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe"); 
     } 
     //2、构造方法 
     private Unsafe() {} 
     //3、初始化方法 
     private static final Unsafe theUnsafe = new Unsafe(); 
     //4、初始化方法实现 
     @CallerSensitive 
     public static Unsafe getUnsafe() { 
         Class<?> caller = Reflection.getCallerClass(); 
         if (!VM.isSystemDomainLoader(caller.getClassLoader())) 
             throw new SecurityException("Unsafe"); 
         return theUnsafe; 
     } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    应用

    从Java 8开始,JDK使用invokedynamic及VM Anonymous Class结合来实现Java语言层面上的Lambda表达式。

    ​ 在Lambda表达式实现中,通过invokedynamic指令调用引导方法生成调用点,在此过程中,会通过ASM动态生成字节码,而后利用Unsafe的defineAnonymousClass方法定义实现相应的函数式接口的匿名类,然后再实例化此匿名类,并返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应Lambda表达式定义逻辑的功能。

    invokedynamic: invokedynamic是Java 7为了实现在JVM上运行动态语言而引入的一条新的虚拟机指令,它可以实现在运行期动态解析出调用点限定符所引用的方法,然后再执行该方法,invokedynamic指令的分派逻辑是由用户设定的引导方法决定。

    VM Anonymous Class:可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过Unsafe.defineAnonymousClass方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何ClassLoader下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的Class对象时,该类就会被GC回收。故而VM Anonymous Class相比于Java语言层面的匿名内部类无需通过ClassClassLoader进行类加载且更易回收。

    5、对象操作

    主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。

    API接口
    //返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    应用

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

    由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。

    6、数组相关

    ​ 主要是用于定位数组中每个元素在内存中的位置,通过arrayBaseOffset与arrayIndexScale这两个方法。

    API接口
    //返回数组中第一个元素的偏移地址
    public native int arrayBaseOffset(Class<?> arrayClass);
    //返回数组中一个元素占用的大小
    public native int arrayIndexScale(Class<?> arrayClass);
    
    • 1
    • 2
    • 3
    • 4
    应用

    ​ 在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用。

    通过Unsafe的arrayBaseOffset、arrayIndexScale分别获取数组首元素的偏移地址base及单个元素大小因子scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位。其中getAndAdd方法即通过checkedByteOffset方法获取某数组元素的偏移地址,而后通过CAS实现原子性操作。

    7、内存屏障

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

    API接口
    //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
    public native void loadFence();
    //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
    public native void storeFence();
    //内存屏障,禁止load、store操作重排序
    public native void fullFence();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    应用

    ​ 在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。

    由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在锁状态校验不准确。所以在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,避免因为重排序而导致数据的不准确型。

    8、系统相关

    API接口
    //返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
    public native int addressSize();  
    //内存页的大小,此值为2的幂次方。
    public native int pageSize();
    
    • 1
    • 2
    • 3
    • 4
    应用

    主要是为java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法,其依赖于Unsafe中pageSize方法获取系统内存页大小实现后续计算逻辑。

    示例

    public class UnsafeTest { 
        public static void main(String[] args) throws Exception { 
            //这里的theUnsafe就是我们源码中的那个theUnsafe 
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); 
            theUnsafe.setAccessible(true); 
            Unsafe unsafe = (Unsafe) theUnsafe.get(null); 
     
            //1、创建对象实例 
            Author author = (Author) unsafe.allocateInstance(Author.class); 
            //2、操作对象的属性 
            Field ageField = Author.class.getDeclaredField("age"); 
            long fieldOffset = unsafe.objectFieldOffset(ageField); 
            //3、操作数组 
            String[] strings = new String[]{"1", "2", "3"}; 
            long i = unsafe.arrayBaseOffset(String[].class); 
            //4、操作内存 
            long address = unsafe.allocateMemory(8L);         
        } 
    } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    补充说明

    ​ 首先在Oracle的Jdk8无法获取到sun.misc包的源码,想看此包的源码可以直接下载openjdk。在IDEA中windows->preference->installed jres->选中jre->edit->rt.jar->source attachment->external folders->openjdk源码路径。此时就可以查看我们的Unsafe类的源码了。

    总结

    ​ Unsafe功能强大,用法广泛,但是若使用不当,会对程序带来许多不可控的灾难。

  • 相关阅读:
    谷粒商城 (四) --------- 项目结构创建 & 初始化数据库
    数据库的约束和设计
    数据库系统原理与应用教程(004)—— MySQL 安装与配置:重置 MySQL 登录密码(windows 环境)
    c#中switch常用模式
    前端工作总结293-uni-增加添加成功提示
    如何构建一个简单的前端框架
    python之Flask入门
    Python爬虫以及数据可视化分析
    C语言——程序解构说明
    vue中使用keep-alive无效以及include 和 exclude用法
  • 原文地址:https://blog.csdn.net/qq_36010886/article/details/133646047