【Java】Unsafe应用解析

目录

一.功能介绍

二.如何获取Unsafe对象

1.从getUnsafe静态方法获取

2.通过反射获取单例对象theUnsafe

三.Unsafe常用API操作

3.1.线程调度

3.1.1 多线程锁

3.1.2 多线程CAS操作

3.1.3 线程的挂起和恢复

3.2.内存屏障

扫描二维码关注公众号,回复: 17272562 查看本文章

 3.3.内存管理

3.4.对象操作

3.5.运行时动态创建类

3.6.Class相关

3.7.数组元素相关

四.总结


最初在看到Java AQS相关代码的时候发现Unsafe在加锁和释放锁时候使用,后来在Java并发编程java.util.concurrent包下经常看到,没有做过多深入研究,直到在Netty框架开发中涉及到了堆外内存(DirectoryByteBuffer)发现了Unsafe的奇妙之处,它避开了Java JVM 堆的内存管理,直接申请分配内存空间,不受JVM GC垃圾回收的限制。当然我们的应用程序运行在Java虚拟机中,JVM的垃圾回收机制释放堆内内存,释放堆内内存的对象所引用的堆外内存的数据结构要不要释放?答案是肯定的。为了巧妙解决这个问题Java提供了虚引用对象(PhantomReference),目的是在JVM GC垃圾回收后触发一个操作:将要释放的堆外内存的对象的引用赋值,然后将对象对应的虚引用Reference放入ReferenceQueue,ReferenceQueue的数据需要手动回收释放堆外内存,这个后续章节可以单独介绍。

Java Unsafe类大家都叫它魔法类,Unsafe类位于rt.jar包下sun.misc包下的一个类,Unsafe类提供了硬件级别的原子操作,类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。由此提供了一些绕开JVM的更底层功能,可以提高程序效率,主要提供一些用于执行低级别、不安全操作的方法,但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

一.功能介绍

要了解Unsafe具备什么能力,首先从宏观上全面看一下(美团技术团队总结如下图)

我们发现Unsafe提供内存操作、CAS操作、Class相关、对象操作、线程调度、内存屏障等操作,接下来结合源码进行分析。

二.如何获取Unsafe对象

首先我们先找到Unsafe类源码,源码可以从Open JDK下载查看OpenJDK8源码下载

public final class Unsafe {

    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }

    private Unsafe() {}

    //私有的对象实例
    private static final Unsafe theUnsafe = new Unsafe();

    //静态方法
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

从上面源码看出获取Unsafe对象可以有以下2个方法:

1.从getUnsafe静态方法获取

其实不然,这个方法在return之前做了一个校验,他会通过VM.isSystemDomainLoader方法校验调用者的ClassLoader,此方法的实现如下

    public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null;
    }

如果调用者的ClassLoader==null,在getUnsafe方法中才可以成功返回实例,否则会抛出java.lang.SecurityException: Unsafe 异常。

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

java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相关方法的类所在jar包路径

2.通过反射获取单例对象theUnsafe

在源码中可以发现,它是用theUnsafe字段来引用unsafe实例,那我们可以尝试通过反射获取theUnsafe字段,进而获取Unsafe实例,代码如下:

public class UnSafeTest {

    @Test
    public void TestUnsafeOffset() throws NoSuchFieldException,IllegalAccessException{
        //获取unsafe对象实例
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(null);

        //获取count属性的Feild
        OrderCount orderCount = new OrderCount();
        Field countField = OrderCount.class.getDeclaredField("count");
        countField.setAccessible(true);

        //获取当前的count值
        int currentCount = countField.getInt(orderCount);
        //计算count内存偏移量countOffset
        int countOffset =  (int)unsafe.objectFieldOffset(countField);
        //原子性修改count的偏移量为10
        unsafe.compareAndSwapInt(orderCount,countOffset,currentCount,10);

        //获取指定偏移量的int值
        System.out.println(unsafe.getInt(orderCount,countOffset));
    }
}
class OrderCount{

    private int count=0;


}

三.Unsafe常用API操作

3.1.线程调度

Unsafe提供了线程调度能力,包括线程锁、CAS操作、线程挂起和恢复等。

3.1.1 多线程锁

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

public class UnSafeTest {

    
    @Test
    public void testLock() throws InterruptedException {
        OrderCount orderCount = new OrderCount();
        //初始化unsafe
        orderCount.initUnsafe();
        //同步线程计数器
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(()->{
            orderCount.addCount(10);
            countDownLatch.countDown();
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                orderCount.addCount(20);
                countDownLatch.countDown();
            }
        }).start();
        countDownLatch.await();
        //输出修改后的值
        System.out.println(orderCount.getCount());
    }
}
class OrderCount{

    private int count=0;
    private  Object lock = new Object();

    private Unsafe unsafe = null;

    public void initUnsafe(){
        //获取unsafe对象实例
        Field field = null;
        try {
            field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }

    //加锁操作
    public void addCount(int addInt){
        unsafe.monitorEnter(lock);
        count+=addInt;
        unsafe.monitorExit(lock);
    }

    public int getCount(){
        return count;
    }
}

3.1.2 多线程CAS操作

Unsafe类的CAS操作可能是用的最多的,常用的几个方法

public final native boolean compareAndSwapObject(Object o, long offset,Object expected,Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,expected,x);

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


    //针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅的偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。所以是一种乐观锁,效率高。expected类似版本号,每次修改需要携带版本号。
    //o:目标Java变量引用,需要变更的目标对象。
    //offset:要修改的属性相对于object对象内存地址的偏移地址。
    //expected:目标Java变量中的目标属性的期望的当前值。x:目标Java变量中的目标属性的目标更新值。
    //类似的方法有compareAndSwapInt,compareAndSwapLong,compareAndSwapObject,在Jdk8中基于CAS扩展出来的方法有getAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
    //对int类型的CAS操作
    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
    //对long类型的CAS操作                                              
    public final native boolean compareAndSwapLong(Object o, long offset,
                                                   long expected,
                                                   long x);


    //获取对象obj 中偏移量为offset 的变量volatile语义的当前值,并设置变量volatile 语义的值为update
    long getAndSetLong(Object obj, long offset, long update)

    //获取对象obj同中偏移量为offset 的变量volatile语义的当前值,并设置变量值为原始值+addValue
    long getAndAddLong(Object obj, long offset, long addValue)

3.1.3 线程的挂起和恢复

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

整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

    //释放被park阻塞的线程,也可以被使用来终止一个先前调用park导致的阻塞,即这两个方法的调用顺序可以是先unpark再park。
    public native void unpark(Object thread);

    //阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)。
    //在time非零的情况下,如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。
    public native void park(boolean isAbsolute, long time);

3.2.内存屏障

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

    //在该方法之前的所有读操作,一定在load屏障之前执行完成。
    public native void loadFence();

    //在该方法之前的所有写操作,一定在store屏障之前执行完成
    public native void storeFence();

    //在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。
    public native void fullFence();

在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要确保数据的一致性。

3.3.内存管理

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

    //获取本地指针的大小(单位是byte),通常值为4(32位系统)或者8(64位系统)。常量ADDRESS_SIZE就是调用此方法。
    public native int addressSize();

    //获取本地内存的页数,此值为2的幂次方。
    //java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法,其依赖于Unsafe中pageSize方法获取系统内存页大小实现后续计算逻辑
    public native int pageSize();

    //分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。
    //bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。
    public native long allocateMemory(long bytes);

    //通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。
    //bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。
    public native long reallocateMemory(long address, long bytes);

    //在给定的内存块中设置值。内存块的地址由对象引用o和偏移地址共同决定,如果对象引用o为null,offset就是绝对地址。第三个参数就是内存块的大小,如果使用allocateMemory进行内存开辟的话,这里的值应该和allocateMemory的参数一致。value就是设置的固定值,一般为0(这里可以参考netty的DirectByteBuffer)。
    //一般而言,o为null,所以有个重载方法是public native void setMemory(long offset, long bytes, byte value);,等效于setMemory(null, long offset, long bytes, byte value);。
    public native void setMemory(Object o, long offset, long bytes, byte value);

    //释放内存
    public native void freeMemory(long address);
    //内存拷贝
    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);

Unsafe分配的内存,不受Integer.MAX_VALUE的限制,并且分配在堆外内存,使用它时需要非常谨慎,忘记手动回收时,会产生内存泄露,可以通过Unsafe#freeMemory方法手动回收;非法的地址访问时,会导致JVM崩溃。

JDK nio包中通过ByteBuffer分配内存就用到了Unsafe的allocateMemory和setMemory方法,具体代码ByteBuffer分配内存方法如下:

    //分配堆外内存DirectByteBuffer
    public static ByteBuffer allocateDirect(int cap) {
        return new DirectByteBuffer(cap);
    }

    //分配堆内内存HeapByteBuffer
    public static ByteBuffer allocate(int cap) {
        if (cap < 0) {
            throw new IllegalArgumentException();
        } else {
            return new HeapByteBuffer(cap, cap);
        }
    }

关于DirectByteBuffer内存分配见另外一篇文章:

【Java】DirectByteBuffer 堆外内存源码解读

查阅了DirectByteBuffer构造函数会发现通过Unsafe内存分配步骤如下:

  1. Bits.reservedMemory();             申请真实的内存大小,并计数
  2. Unsafe.allocateMemory(cap);        分配内存
  3. ​​​​​​​Bits.unreserveMemory();            分配失败则重置申请参数
  4. Unsafe.setMemory();                初始化内存
  5. Unsafe.freeMemory();                                         释放内存

堆外内存释放通过虚引用Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放(通过在Cleaner中调用Unsafe#freeMemory方法),虚引用Cleaner对象回收堆外内存后边文章结合JDK Reference再做介绍。

3.4.对象操作

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

    /*1.获取对象字段的值*/

    //通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。
    // 类似的方法有getInt、getDouble等等。
    public native Object getObject(Object o, long offset);

    //此方法和上面的getObject功能类似,不过附加了'volatile'加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。
    // 这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。
    public native Object getObjectVolatile(Object o, long offset);

    /*2.修改对象字段的值*/

    //设置Java对象o中偏移地址为offset的属性的值为x,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。用于修改修改非基本数据类型的值。
    //类似的方法有putInt、putDouble等等,用于修改基本数据类型的值,再次不再赘述。
    public native void putObject(Object o, long offset, Object x);


    //此方法和上面的putObject功能类似,不过附加了'volatile'加载语义,也就是设置值的时候强制(JMM会保证获得锁到释放锁之间所有对象的状态更新都会在锁被释放之后)更新到主存,从而保证这些变更对其他线程是可见的。
    // 类似的方法有putIntVolatile、putDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和putObject方法相同。
    public native void putObjectVolatile(Object o, long offset, Object x);

    //设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。
    // 只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。
    // 最终会设置成x,但是可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。
    public native void putOrderedObject(Object o, long offset, Object x);


    /*3.获取对象的字段相对该对象地址的偏移量*/

    //返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。即相对于 className.class 的偏移量,通过这个偏移量可以快速定位字段.
    // 注意:这个方法仅仅针对静态属性,使用在非静态属性上会抛异常。
    public native long staticFieldOffset(Field f);

    //返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。即字段到对象头的偏移量,通过这个偏移量可以快速定位字段.
    // 注意:这个方法仅仅针对非静态属性,使用在静态属性上会抛异常。
    public native long objectFieldOffset(Field f);

    //返回给定的静态属性的位置,配合staticFieldOffset方法使用。实际上,这个方法返回值就是静态属性所在的Class对象的一个内存快照
    // 注释中说到,此方法返回的Object有可能为null,它只是一个'cookie'而不是真实的对象,不要直接使用的它的实例中的获取属性和设置属性的方法,它的作用只是方便调用上面提到的像getInt(Object,long)等等的任意方法。
    public native Object staticFieldBase(Field f);

    /*4.创建对象*/   
    //绕过构造方法、初始化代码来非常规的创建对象,注意创建对象只是分配对象地址,不负责对象属性初始化
    public native Object allocateInstance(Class<?> cls) throws InstantiationException;

 以上每一个方法都可以通过代码实例来测试。

3.5.运行时动态创建类

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

public class DynamicCreateClass {
    private static Unsafe unsafe;
    //初始化Unsafe对象
    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //加载类字节码内容
    private static byte[] getClassContent() throws Exception {
        File f = new File("target/classes/com/thread/test/juc/unsafe/A.class");
        FileInputStream input = new FileInputStream(f);
        byte[] content = new byte[(int) f.length()];
        input.read(content);
        input.close();
        return content;
    }
    public static void main(String[] args) throws Exception {
        //Sample code to creat classes
        byte[] classContents = getClassContent();
        //通过unsafe的defineClass根据字节码内容生成class类类型对象
        Class c = unsafe.defineClass(null, classContents, 0, classContents.length, CreateClass.class.getClassLoader(), null);
        c.getMethod("a").invoke(c.newInstance());   //aaaa
    }
}
class A {
    public void a() {
        System.out.println("aaaa");
    }
}

3.6.Class相关

通过2.5测试了动态生成Class,Unsafe还提供了以下Class操作Api

    //检测给定的类是否需要初始化。通常需要使用在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)。
    //此方法当且仅当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);


    ///定义一个匿名类,与Java8的lambda表达式相关,会用到该方法实现相应的函数式接口的匿名类,可以看结尾文章链接。
    public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

3.7.数组元素相关

    //返回数组类型的第一个元素的偏移地址(基础偏移地址)。如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。
    // Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。
    public native int arrayBaseOffset(Class<?> arrayClass);

    //返回数组单个元素的大小,数组中的元素的地址是连续的。
    // Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。
    public native int arrayIndexScale(Class<?> arrayClass);

四.总结

官网温馨提示:

Although, Unsafe has a bunch of useful applications, never use it.

尽管Unsafe很有用,绝不能使用它

通过对Unsafe提供的API整理发现Unsafe在以下几个方面提供了优异的能力:

内存管理

  • 通过Unsafe实现堆外内存的分配,绕过Jvm垃圾回收的管理,避免了堆内内存扩大造成过多的垃圾回收影响性能,同时提供了堆外内存拷贝和释放等能力。需要注意的是使用Unsafe堆外内存需要手动释放,处理不好会导致堆外内存泄露。
  • Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。

线程调度:

  • 通过Unsafe实现多线程锁、多线程CAS操作、线程挂起和恢复,这些功能在AQS中以及Java多线程并发处理concurrent包下使用最为广泛,工作中直接使用即可没有必要使用Unsafe操作,避免使用不当造成风险。

其他注意事项

  • 其他更多的API操作如内存屏障、Class相关、对象操作日常开发有需要可以直接查阅源码进行调用即可。
  • Unsafe是“不安全”的,这里的不安全指的是不合理使用其API会产生无法预估的后果,并不是Unsafe不安全。使用它有一定的门槛,操作不当会让你的应用直接crash,所以不建议直接使用。
  • 未来Unsafe是否会从JDK中移除没有定论,为了避免未来移除Unsafe给程序带来的不可兼容性,所以谨慎使用。

参考文章

view src/share/classes/sun/misc/Unsafe.java @ 14626:7fcf35286d52

Java Magic. Part 4: sun.misc.Unsafe

JAVA中神奇的双刃剑--Unsafe

JVM源码分析之堆外内存完全解读

Java魔法类:Unsafe应用解析

Java中Unsafe类的原理详解与使用案例

猜你喜欢

转载自blog.csdn.net/smallbirdnq/article/details/133670739