深入理解Java魔法类Unsafe

前言

​ Unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。其实例一般情况是获取不到的,源码中的设计是采用单例模式,不是启动类加载器加载初始化就会抛SecurityException异常。

​ 这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是不安全的,它所分配的内存需要手动free(不被GC回收)。如果对Unsafe类理解的不够透彻,就进行使用的话,就等于给自己挖了无形之坑,最为致命。

​ 由于sun并没有将其开源,也没给出官方的Document。但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的C/C++代码,但求能了解它的基本功能。

一、获取Unsafe实例

Unsafe类是不可以直接new出来,因为它把构造方法是私有化了,同时虽然他也提供了一个getUnsafe()静态方法,但是如果直接调用的这个方法的话就会抛出SecurityException异常。因为我们在外部调用的时候使用的不是启动类加载器,人家这个类就要求必须使用启动类加载器才可以正常使用。

VM类中的isSystemDomainLoader()方法

它在返回实例之前会获取当前调用者的类加载器,如果不是启动类加载器就会抛出SecurityException异常。

普通用户获取Unsafe实例的正确姿势

查看源码,我们发现它有一个属性叫theUnsafe,我们直接通过反射拿到它即可。

public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Class<Unsafe> clazz = Unsafe.class;
        Field field = clazz.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe);
    }
}

二、底层native方法汇总

共计82个public native,下面列出了核心方法:

   //扩充内存  
    public native long reallocateMemory(long address, long bytes);  
      
    //分配内存  
    public native long allocateMemory(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);  
      
    //获取值,不管java的访问限制,其他有类似的getInt,getDouble,getLong,getChar等等  
    public native Object getObject(Object o, long offset);  
      
    //设置值,不管java的访问限制,其他有类似的putInt,putDouble,putLong,putChar等等  
    public native void putObject(Object o, long offset);  
      
    //从一个给定的内存地址获取本地指针,如果不是allocateMemory方法的,结果将不确定  
    public native long getAddress(long address);  
      
    //存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的,结果将不确定  
    public native void putAddress(long address, long x);  
      
    //该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的  
    public native long staticFieldOffset(Field f);  
      
    //报告一个给定的字段的位置,不管这个字段是private,public还是保护类型,和staticFieldBase结合使用  
    public native long objectFieldOffset(Field f);  
      
    //获取一个给定字段的位置  已经过时
    public native Object staticFieldBase(Field f);  
      
    //确保给定class被初始化,这往往需要结合基类的静态域(field)  
    public native void ensureClassInitialized(Class c);  
      
    //可以获取数组第一个元素的偏移地址  
    public native int arrayBaseOffset(Class arrayClass);  
      
    //可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用, 可以定位数组中每个元素在内存中的位置  
    public native int arrayIndexScale(Class arrayClass);  
      
    //获取本机内存的页数,这个值永远都是2的幂次方  
    public native int pageSize();  
      
    //告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类  
    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);  
      
    //锁定对象,必须是没有被锁的 已经过时
    public native void monitorEnter(Object o);  
      
    //解锁对象  已经过时
    public native void monitorExit(Object o);  
      
    //试图锁定对象,返回true或false是否锁定成功,如果锁定,必须用monitorExit解锁  已经过时
    public native boolean tryMonitorEnter(Object o);  
      
    //引发异常,没有通知  
    public native void throwException(Throwable ee);  
      
    //CAS,如果对象偏移量上的值=期待值,更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。  
    public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);  
      
    // 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile,getBooleanVolatile等等  
    public native Object getObjectVolatile(Object o, long offset);   
      
    //线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
    public native void park(boolean isAbsolute, long time);  
      
    //终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,也正是使用这两个方法
    public native void unpark(Object thread);  
      
    //获取系统在不同时间系统的负载情况  
    public native int getLoadAverage(double[] loadavg, int nelems);  
      
    //创建一个类的实例,不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例,对于单例模式,简直是噩梦,哈哈  
    public native Object allocateInstance(Class cls) throws InstantiationException;

三、Unsafe的使用示例

1、使用Unsafe实例化一个类

有一个User

public class User {
     String name;
     int age;

    public User(){
        name="小明";
        age=10;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", stamp=" + stamp +
                '}';
    }
}

通过Unsafe的allocateInstance()方法实例化该对象

package top.easyblog;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * Unsafe类灵魂追问:
 * (1)Unsafe是什么?
 * <p>
 * (2)Unsafe只有CAS的功能吗?
 * <p>
 * (3)Unsafe为什么是不安全的?
 * <p>
 * (4)怎么使用Unsafe(怎么获得Unsafe的实例)?
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/07 11:40
 */
public class UnsafeTest {

    private static Unsafe unsafe;

    static {
        Class<Unsafe> unsafeClass = Unsafe.class;
        Field field = null;
        try {
            field = unsafeClass.getDeclaredField("theUnsafe");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        assert field != null;
        field.setAccessible(true);
        try {
            unsafe = (Unsafe) field.get(null);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        System.out.println(instanceFor(unsafe, User.class));
    }

    /**
     * 使用Unsafe实例化一个类
     *
     * @param clazz
     * @param <T>
     */
    public static <T> T instanceFor(Unsafe unsafe, Class<T> clazz) throws InstantiationException {
        return (T) unsafe.allocateInstance(clazz);
    }
}

执行结果:

从打印的结果来看,字段全是默认初始值,没有执行构造方法初始化。其实allocateInstance()方法只会给对象分配内存,并不会调用构造方法。你问我这个用啥用?这个对于单例模式简直是无解的开挂行为,因为他实例化对象不需要经过构造器。

2、修改私有字段的值

使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。

public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        Class<Unsafe> unsafeClass = Unsafe.class;
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);

        User user =new User();
        Field age = user.getClass().getDeclaredField("age");
        //使用unsafe.putXxx给属性设值
        unsafe.putInt(user,unsafe.objectFieldOffset(age),30);
        System.out.println(user);
    }

}

这里我们可以配合第1种用法来给一个对象分配内存以及初始化。(当然我们也可以通过反射直接修改。)

3、抛出checked异常

我们知道如果代码抛出了checked异常,要不就使用try…catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。

// 使用Unsafe抛出异常不需要定义在方法签名上往外抛,直接使用API的方式就可以了
public static void throwException() {
    unsafe.throwException(new Exception());
}
4、在堆外分配内存

使用java 的new会在堆中为对象分配内存,并且对象的生命周期内,会被JVM GC管理。
Unsafe分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露;非法的地址访问时,会导致JVM崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM延迟)时,可以使用它。java.nio使用这一技术。Spark中的Netty也使用了这个技术。

假设我们要在堆外创建一个巨大的byte数组,我们可以使用allocateMemory()方法来实现:

OffHeapArray

package top.easyblog;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 堆外数组
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/07 14:59
 */
public class OffHeapArray {

    private static final int BYTE = 1;
    private long size;
    private long array;
    private static Unsafe unsafe;

    static {
        try {
            Field thUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            thUnsafe.setAccessible(true);
            unsafe = (Unsafe) thUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    //构造方法中给数组分配内存,理论上可以分配任意大小的数组
    public OffHeapArray(long size) {
        this.size = size;
        array = unsafe.allocateMemory(size * BYTE);
    }


    /**
     * 获得索引i处的元素
     *
     * @param i
     * @return
     */
    public int get(long i) {
        return unsafe.getInt(array + i * BYTE);
    }

    /**
     * 设置值
     *
     * @param i
     * @param value
     */
    public void set(long i, int value) {
        unsafe.putInt(array + i * BYTE, value);
    }

    /**
     * 返回数组大小
     *
     * @return
     */
    public long length() {
        return this.size;
    }

    /**
     * 释放内存
     */
    public void free() {
        unsafe.freeMemory(array);
    }

    public static void main(String[] args) {
        System.out.println(unsafe);
    }
}
OffHeapArray offHeapArray = new OffHeapArray((long)Integer.MAX_VALUE+100);
offHeapArray.set(0, 45);
System.out.println("索引0:"+offHeapArray.get(0));
System.out.println("数组大小:"+offHeapArray.length());
//用完之后一定要记得手动释放内存
offHeapArray.free();

执行结果:

你问我这个有啥用?Java的数组最大容量受常量Integer.MAX_VALUE的限制,如果我们用直接申请内存的方式去创建数组,那么数组大小只会收到堆的大小的限制。当时使用这个方式理论上可以创建任意大小的数组。

5、CAS操作

J.U.C底层大量使用了CAS,在AQS、ConcurrentHashmap、ForkJoinPool、FutureTask、StampedLock等都有大量的应用。它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。

6、park/unpark

在LockSupport类中有两个静态方法park()和unpark(),他们的底层调用的就是Unsafe类中对应的park()/unpark()本地方法,当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。具体的分析请参考我的另一篇博客LockSupport深入源码剖析

7、计算对象的内存大小

基本的思路如下:

(1)通过反射获得一个类的Field

(2)通过Unsafe的objectFieldOffset()获得每个Field的offSet
(3)对Field按照offset排序,取得最大的offset,然后加上这个field的长度,再加上Padding对齐

public class UnsafeTest {

    private static Unsafe unsafe;

    static {
        Class<Unsafe> unsafeClass = Unsafe.class;
        Field field = null;
        try {
            field = unsafeClass.getDeclaredField("theUnsafe");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        assert field != null;
        field.setAccessible(true);
        try {
            unsafe = (Unsafe) field.get(null);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        System.out.println(sizeOf(new Student()));
    }

    /**
     * 计算一个对象的大小,单位:byte
     *
     * @param object
     * @return
     */
    public static long sizeOf(Object object) {
        HashSet<Field> fields = new HashSet<>();
        Class<?> clazz = object.getClass();
        while (clazz != Object.class) {
            for (Field field : clazz.getDeclaredFields()) {
                if ((field.getModifiers() & Modifier.STATIC) == 0) {
                    fields.add(field);
                }
            }
            clazz = clazz.getSuperclass();
        }

        long maxSize = 0;
        for (Field field : fields) {
            long offset = unsafe.objectFieldOffset(field);
            if (offset > maxSize) {
                maxSize = offset;
            }
        }
        return ((maxSize / 8) + 1) * 8;   //对齐填充
    }
}

Student类,有一个String类型的引用4字节,int类型4字节,然后MarkWord 8字节,类型指针4字节,这总共8+4+4+4=20字节,然后需要对齐填充到最近的8字节整数倍,应该是24字节。

public class Student {

    String name;   //4B
    int age;      //4B

    //8+4+8 ==> 24B

}

执行结果:

可以看到执行结果符合我们的预期值。

留言区

还能输入500个字符